进程和线程的本质

谁说只有细胞可以分裂的

高中的时候,大家都学过,细胞是可以分裂的,在 Linux 系统里面,进程也是可以分裂的。细胞分裂靠的是 DNA 和蛋白质,进程分裂靠的是 task_struct 和 fork().

task_struct {
 pid , 
 ...
 mm , -- 内存地址
 fs , -- 和进程相关的路径
 files -- 文件资源
}

大家回忆一下高中时候的知识, DNA 是模板,是设计图纸,蛋白质是按照图纸制造新细胞的。同样的 task_struck 描述一个进程的基本信息,像极了细胞的 DNA 。蛋白质根据 DNA 序列,制作出新的蛋白质和DNA,fork 像极了蛋白质。请看下面的图。

进程的分裂

进程和线程的本质_第1张图片
细胞分裂:

进程和线程的本质_第2张图片进程的分裂还是有点不一样的。

细胞分类以后,就成了两个细胞了。进程分裂以后,除了 pid 相同之外,其他的都是一样的。有人说了,连内存都相同吗?还真是相同。请看下面的图。
进程和线程的本质_第3张图片

talk is cheap show me code :


#include 
#include 
#include 
#include 
int data = 10;
int child_process()
{
        printf("Child process %d, data %d\n",getpid(),data);
        data = 20;
        printf("Child process %d, data %d\n",getpid(),data);
        _exit(0);
}
int main(int argc, char* argv[])
{
        int pid;
        pid = fork();

        if(pid==0) {
                child_process();
        }
        else{
                sleep(1);
                printf("Parent process %d, data %d\n",getpid(), data);
                exit(0);
        }
}

进程线程傻傻分不清

cow 是依赖于 mmu(内存管理单元),这是一个硬件单元。执行完 fork 之后,mmu 设置 readonly ,当有进程需要修改内存内容的时候,mmu 发生 page fault ,然后新建一段内存给申请修改操作的进程,然后再对内存进行修改。

如果没有 mmu 的话,我们 linux 只提供了 vfork 了, vfork 在内存是这样的。他没有 cow ,干脆内存就一样吧。所以 vfork 的过程就变成了下面的样子:

进程和线程的本质_第4张图片
talk is cheep ,show me the code :

     1  #include <stdio.h>
     2  #include <sched.h>
     3  #include <unistd.h>
     4
     5  int data = 10;
     6
     7  int child_process()
     8  {
     9          printf("Child process %d, data %d\n",getpid(),data);
    10          data = 20;
    11          printf("Child process %d, data %d\n",getpid(),data);
    12          _exit(0);
    13  }
    14
    15  int main(int argc, char* argv[])
    16  {
    17          if(vfork()==0) {
    18                  child_process();
    19          }
    20          else{
    21                  sleep(1);
    22                  printf("Parent process %d, data %d\n",getpid(), data);
    23          }
    24  }

运行这点代码的结果是

Child process 1345, data 10
Child process 1345, data 20
Parent process 1344, data 20

由结果可以看到 子进程和父进程中的 data 是不一样的。

这里有一个问题,既然进程和线程之间的区别在于是否共享内存,里面 task_struct 是一样的。按照上面的说法,进程里面的线程 pid 是不一样的,为什么我们 top 看到的 pid 都是一样的呢?看看下面的 code 吧!


     1  #include <stdio.h>
     2  #include <pthread.h>
     3  #include <stdio.h>
     4  #include <linux/unistd.h>
     5  #include <sys/syscall.h>
     6
     7  static pid_t gettid( void )
     8  {
     9          return syscall(__NR_gettid);
    10  }
    11
    12  static void *thread_fun(void *param)
    13  {
    14          printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
    15          while(1);
    16          return NULL;
    17  }
    18
    19  int main(void)
    20  {
    21          pthread_t tid1, tid2;
    22          int ret;
    23
    24          printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
    25          /*new a thread and call thread_fun*/
    26          ret = pthread_create(&tid1, NULL, thread_fun, NULL);
    27          if (ret == -1) {
    28                  perror("cannot create new thread");
    29                  return -1;
    30          }
    31          /*new another thread and call thread_fun*/
    32          ret = pthread_create(&tid2, NULL, thread_fun, NULL);
    33          if (ret == -1) {
    34                  perror("cannot create new thread");
    35                  return -1;
    36          }
    37          /*main thread use pthread_join to wait a sub_thread */
    38          if (pthread_join(tid1, NULL) != 0) {
    39                  perror("call pthread_join function fail");
    40                  return -1;
    41          }
    42          /*at the same time . main thread use pthread_join to wait another sub_thread */
    43          if (pthread_join(tid2, NULL) != 0) {
    44                  perror("call pthread_join function fail");
    45                  return -1;
    46          }
    47
    48          return 0;
    49  }

结果是:

thread pid:1554, tid:1554 pthread_self:139970410002240
thread pid:1554, tid:1555 pthread_self:139970401605376
thread pid:1554, tid:1556 pthread_self:139970393212672

pstree 看一下:
进程和线程的本质_第5张图片
看到进程树,我想到一个问题,ps 或者 top 进去里面的 pid 是父进程的进程号还是子进程或者子线程的 pid。这就是引出了一个问题——GTID(global task id) ,GTID 就是一个总代理,他的后面是一个一个进程线程ID,我用下面的图来说明我的想法:

进程和线程的本质_第6张图片这就是 GTID 了,明白了很简单。

父亡儿何去

进程和线程的本质_第7张图片
在上面的图中,我们可看到一个树状的进程关系实例。假设,两个 parent 和 Child 进程死掉以后,那子进程改如何该挂到谁的底下。

进程和线程的本质_第8张图片子进程有两个选择,一个是找 init 线程,一个是 subreaper 线程。

talk is sheep show me the code .

1  #include <stdio.h>
2  #include <sys/wait.h>
3  #include <stdlib.h>
4  #include <unistd.h>
5
6  int main(void)
7  {
8          pid_t pid,wait_pid;
9          int status;
10
11          pid = fork();
12
13          if (pid==-1)    {
14                  perror("Cannot create new process");
15                  exit(1);
16          } else  if (pid==0) {
17                  printf("child process id: %ld\n", (long) getpid());
18                  pause();
19                  _exit(0);
20          } else {
21                  printf("parent process id: %ld\n", (long) getpid());
22                  wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED);
23                  if (wait_pid == -1) {
24                          perror("cannot using waitpid function");
25                          exit(1);
26                  }
27
28                  if(WIFSIGNALED(status))
29                          printf("child process is killed by signal %d\n", WTERMSIG(status));
30
31                  exit(0);
32          }
33  }

上面的程序跑起来后,会 fork 出一个子进程。我们 kill 掉父进程后,看看情况。如下:
进程和线程的本质_第9张图片
上图中可以看到 18060 是父进程,18064 是 18060 的子进程,现在再来看一下 pstree 的结果:

进程和线程的本质_第10张图片kill 掉父进程的如下:

进程和线程的本质_第11张图片
这力在认识一下,Linux 的目录/proc目录里面的好东东。

执行ls /proc | egrep '^d',会发现在这个目录下面有许多以数字为名字的文件,这些可不是普通的文件,这些文件正式 Linux 内核提供给我们的线程的信息。
进程和线程的本质_第12张图片
例如,我们执行之前写的多进程的例子。
进程和线程的本质_第13张图片
当我们执行./a.out后,在/proc的目录地下会生成三个文件:

进程和线程的本质_第14张图片
这三个文件就对应了这三个进程的进程号。
其中每个文件的 task 子文件夹中,我们都可以找到下面的内容:
进程和线程的本质_第15张图片
还有更有趣的东西,你会发现这三个“进程”是一个爹。

进程和线程的本质_第16张图片
在这里插入图片描述

再来看看 pstree 里面是如何显示的:
进程和线程的本质_第17张图片
在 pstree 的手册中,我们可以看到a.out───2*[{a.out}]。其中 [{}] 里面包含的是父进程的两个子线程,而且两个子线程的名字是一样的。在代码里面我看到使用了 create_thread 函数新建的子线程。在这里可以得出结论,父进程和子矜持之间的内存是共享的,连 proc 里面的东西都是一样的。

pstree 的手册里面,还说了父进程下面的名字重复的子进程使用[]表示的。请看下面的代码:

1  main()
2  {
3          fork();
4          printf("hello\n");
5          fork();
6          printf("hello\n");
7          while(1);
8  }

pstree 以后:
进程和线程的本质_第18张图片从上面图里面很容易看出下面进程之间的父子关系。

1540--|----1541
          |-----1542

在来看看 proc 里面的东西:
![task 里面的东西](https://img-blog.csdnimg.cn/2020052821334739.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JsdWVkcmFhbV9wcA==,size_16,color_FFFFFF,t_70
进程和线程的本质_第19张图片

进程要的睡眠

为什么进程需要睡眠?从根本上讲还是 cpu、内存、磁盘、网络处理数据的不同的速度造成的。cpu 处理数据太快了,其他的兄弟们都跟不上节奏。网络模块辛辛苦苦弄了一点数据过来,就让 cpu 秒杀了,等下一波数据来的话,需要等一下。cpu 可以计算里面最珍贵的资源,怎么能让他闲着呢,哪怕 1 us 都不行的,1 us 已经能干好多事情了,所以在 Linux 里面,进程本着大公无私的进程主动挪挪地方,去睡觉吧,资源来了,我在运行,我的运行的上下文,但是进程调度器先替我保存一下我的上下文。

那睡眠和停止(stop)之间有什么区别呢?stop 是其他的进程给进程一个信号,你先暂停一下。“睡眠”是说,我的资源没来了,让其他进程先用 cpu 吧。睡眠是进程主动让出 cpu。停止是赶鸭子下架,强行的把进程从 cup 上拉下来。

深睡眠是是雷打不动那种,谁来叫都不行,让我醒来也可以,条件只要一个我等的资源来了就可以了
浅睡眠是雷一打就动,一有其唤醒的信号进来,我就醒了。

进程和线程的本质_第20张图片

“鸡”、“蛋”问题

Linux 系统中,存在一个创世进程——进程中的德古拉伯爵。就是 0 号进程。0 号进程的任务两个任务。下面是一个:

0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程

ok,简单的说就是研究“生”,繁殖后代。
另外一个任务是蜕化成 IDLE 进程,当所有的进程都睡了,就该他上场了。这是调度算法决定的,这个时候,cpu 进入低功耗状态。

你可能感兴趣的:(Linux)