进程的一生(二)

进程的fork()

我们知道fork()创建的是一个进程,如下图在进程p1中fork出一个子进程p2,p1和p2都有各种的task_struct,在内核的角度上只认得这个task_stuct,只要你是个task_struct就能被调度,linux里面有这么一个逻辑,当p1把p2创建出来的时候,p1就将描述资源的结构对拷给p2,比如我们p1中的当前路径资源也拷贝给了p2,既然是两个进程,那你的资源和我的资源一致那还叫什么进程?所以linux内核中做了这么一件事,只要谁动了资源就要分裂,比如说chroot,open,写memory,mmap,sigaction…
在这里插入图片描述
看一个例子

#include <stdio.h>
#include <sched.h>
#include <unistd.h>
int data = 100;
int child_work()
{
  printf("Child process %d,data %d\n",getpid(),data);
  data = 66;
  printf("Child process %d,data %d\n",getpid(),data);
  _exit(0);
}
int main()
{
      if(fork()==0)
      {
         child_work();
      }
      else
      {
        sleep(2);
        printf("Parent process %d, data %d\n",getpid(),data);
      }
}

结果:

sice@sice:~$ ./a.out 
Child process 8880,data 100
Child process 8880,data 66
Parent process 8879, data 100

我们上面提到,我们fork()出来的是进程,谁动了资源都要分裂,所以我们写这个data,要把data进行分裂,这个分裂的原理是什么呢?这个其实就是写拷贝技术,见下图,最开始p1中的data访问通过虚拟地址经过MMU转化后变成物理地址去访问物理内存,这时候该物理内存段是可读可写的,当p1 fork()出一个子进程后,该物理内存段就改为只读属性,但是此时的data的虚拟地址和物理地址还是不变,当我们去写data时,此时就发生了变化,物理地址被拷贝出来一份,即使进程p1和p2中的data虚拟地址是一样的,但是它们的物理地址是不同的,物理内存段也变成了可读可写属性,之后父子进程都可以去写这个data了,所以当子进程写这个data的时候并不会改变父进程的data值,这个写时拷贝技术严重依赖于MMU,在没有MMU的系统中,是没有copy_on_write的,没有fork的,只有vfork,vfork区别于fork的重点是vfork会阻塞父进程直到子进程调用exit或者exec
在这里插入图片描述
vfork()示意图如下,可以看出p1进程结构体中的task_struct不再复制一份mm指针,mm指针指向同一片内存资源,
我们用vfork()代替fork()改写上面的例子,结果如下,可以看出父子进程指向同一片内存资源
在这里插入图片描述

Child process 8893,data 100
Child process 8893,data 66
Parent process 8892, data 66

在线程的世界里当p1使用pthread_create()创建子线程p2的时候,将p1和p2使用所有同样的线程资源,不再拷贝一份,共享线程资源,可调度
在这里插入图片描述

PID和TGID

在linux里面呢,POSIX标准要求,一个进程里面有多个线程的话,必须向上面看起来像一个整体,就是我们去getpid的时候会得到同一个pid,本来我们的P1,P2,P3,P4应该有自己的pid,但是linux要求在P1,P2,P3,P4线程里getpid()应该得到同一个pid,linux在内核里面搞一个"很假"的TGID,它让P1,P2,P3,P4的PID1,PID2,PID3,PID4都等于PID1,所以我们在用户空间去getpid()的时候会获得同一个pid,得到的其实是TGID,如下图所示
在这里插入图片描述
现在我们来看一个例子

#include <stdio.h>
#include <pthread.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
static pid_t gettid(void)
{
  return syscall(__NR_gettid);
}
static void*thread_fun(void *param)
{
   printf("thread pid:%d,tid:%d pthread_self:%lu\n",getpid(),gettid(),pthread_self());
   while(1);
   return NULL;
}
int main(void)
{
   pthread_t tid1,tid2;
   printf("thread pid:%d,tid:%d pthread_self:%lu\n",getpid(),gettid(),pthread_self());
   pthread_create(&tid1,NULL,thread_fun,NULL);
   pthread_create(&tid2,NULL,thread_fun,NULL);
   pthread_join(tid1,NULL);
   pthread_join(tid2,NULL);
   return 0;
}

sice@sice:~$ ./a.out 
thread pid:8934,tid:8934 pthread_self:3075696320
thread pid:8934,tid:8936 pthread_self:3067300672
thread pid:8934,tid:8935 pthread_self:3075693376

通过上面的例子可以看出任何一个线程getpid()得到的是同一个pid,即tgid,当我们调用gettid()得到是线程正真的pid,也验证了上文中加黄色的描述
在这里插入图片描述
当我们使用top命令的时候,可以看出内核把多个线程看出一个整体,该整体的标识就是tgid8934因为我们在线程中执行while(1),所以占用率极高,当使用top -H看到是线程真正的pid

SUBREAPER和托孤

在这里插入图片描述
如上图所示,当p2,p4死的时候,p3和p5成为了孤儿进程,现在它们有两个选择,可以托付给最近的进程或者托付给init进程,该最近的进程称为"SUBREAPER"收割机,我们先来看下面的例子

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{ 
   pid_t pid,wait_pid;
   int status;
   pid = fork();
   if(pid==-1)
   {
      perror("Cannot create new process");
      exit(1);
   }
   else if(pid == 0)
   {
      printf("Child process id:%ld\n",(long)getpid());
      pause();
      _exit(0);
   }
   else
   {
     printf("Parent process id:%ld\n",(long)getpid());
     wait_pid = waitpid(pid,&status,WUNTRACED|WCONTINUED);
        if(wait_pid == -1)
        {
          perror("cannot using waitpid function");
          exit(1);
        }
        if(WIFSIGNALED(status))
          printf("child process is killed by signal %d\n",WTERMSIG(status));
          exit(0);
   }
}

把父进程干掉前的进程树
在这里插入图片描述
把父进程干掉后的进程树
在这里插入图片描述
很明显,子进程运气不好找不到它的收割机,托付给了init进程

进程0和1

在这里插入图片描述
可以通过上图看出,init进程是一号进程,它的父进程是0号进程,但是我们在pstree的时候为什么看不到0进程呢,这是因为0进程创建出1进程后就被退化成IDLE进程,当我们所有进程睡眠的时候系统就去调度进程0跑,把cpu变成低功耗并且保持系统不死机

发布了83 篇原创文章 · 获赞 3 · 访问量 1242

猜你喜欢

转载自blog.csdn.net/qq_41936794/article/details/105367502