进程创建,进程等待,进程终止,进程的程序替换

进程创建

fork()函数:
在原来的博客中已经写过一次进程创建了,在这儿就详细讲解一下进程是怎么创建的.
已经知道,在Linux中,创建进程可以通过调用fork函数来实现,那么当我们在调用fork函数时,操作系统内部到底是怎样实现的呢?

fork()函数在内部通过已经存在的进程创建出一个新进程,而新进程就是子进程.
#include<unistd.h>
pid_t fork(void);
有两个返回值,当返回值大于零时,返回的是父进程;等于零时,返回的是子进程;出错则返回1.

进程调用fork()函数时,在操作系统内部应该完成以下步骤:

  • 分配新的内存块和内核数据结构给子进程.
  • 将父进程数据结构部分内容拷贝至子进程.(父子进程代码和数据部分共享,但当有一方要进行写入时,就会发生写时拷贝.所以当在父子进程中有相同的变量,而改变子进程中变量的值时,父进程中的变量并不会改变.)
  • 添加子进程到系统进程列表中.
  • fork()返回,开始调度器调度.

如下图所示:
当调用fork()函数时.
这里写图片描述
调用fork()函数结束后.如下图,就生成了一个新进程.并且在此时父子进程同时从fork()函数结束后的下一步开始执行(也就是图中的after那步),而对于到底是父进程先执行还是子进程先执行,这是由操作系统调度器决定的,而我们并不知道.
这里写图片描述
当父子进程中有一方的数据要进行写入时,就会发生写实拷贝,而代码部分是相同的不会被改变,所以代码部分父子进程共享.
通过下面这个图我们就可以形象的观察到:
这里写图片描述
注意:在我们过程中使用的内存,都是虚拟内存,而不是真正的物理内存,在前面的博客中,我详细的讲解过虚拟内存,若想了解请戳:虚拟内存
那么在这儿,在实现写时拷贝时,虚拟内存是如何找到对应的物理内存并且实现写时拷贝的呢?
这里写图片描述
拷贝也就是实现页表的拷贝.
vfork()函数
作用:
也是用来创建子进程.
特点:

  • 创建一个子进程,而子进程和父进程共享地址空间,但是fork()函数创建的子进程具有独立的地址空间.
  • 保证子进程先运行,当调用了exec()或者_exit()函数后父进程才能被调度运行.
    简单说一下exec()函数:当进程调用exec()函数时,该进程的用户空间和数据完全被新程序替换,从新程序的启动处(main()函数为入口)开始执行.调用exec()函数并不创建新进程,所以调用exec()函数前后该进程的ID并未改变.
    我们可以通过下面的代码来简单模拟:
#include<unistd.h>
  2 #include<stdio.h>
  3 int g_val = 100;
  4 int main()
  5 {
  6     pid_t pid = vfork();                                                                                                
  7     if(pid == 0)
  8     {
  9         sleep(3);
 10         g_val = 200;
 11         printf("son:%d\n",g_val);
 12         _exit(0);
 13     }
 14     else if(pid > 0)
 15     {
 16         printf("father:%d\n",g_val);
 17     }
 18     else
 19     {
 20         perror("vfork");
 21     }
 22     return 0;
 23 }               

这里写图片描述
所以:vfork()函数有很大的缺陷.

进程终止

一个进程可以通过那些方式正常的退出:

  • 从main()函数返回,即通过return退出.
  • 调用exit()函数 //库函数
  • 调用_exit()函数 // 系统调用函数
    当我们使用crtl + c退出一个进程时,这属于异常退出.
    下面我们来区分一下这两种退出函数:
    _exit()函数:
#include<unistd.h>
void _exit(int status);
void exit(int status);

上述的两个函数都是输出型函数,所以在传status进去时,可以直接传零进去.status:定义了进程的终止状态,父进程可以通过wait()函数(在该篇博客的后面讲解)来获取该值.敲黑板敲黑板:虽然status是int类型,但是只有低8位可以被父进程使用.所以当status的值为-1时,返回的结果就是255.此时,还有一种查看退出码的方法(就是函数执行结束后的返回结果):echo $?
那exit()函数和_exit()函数有什么不同呢?
在调用exit函数时,会先执行以下步骤:

  1. 执行用户的清理函数atexit()和unexit()函数.
  2. 冲刷缓冲区,关闭流等.
  3. 调用_exit()函数.

进程等待

在前面的博客中讲到,当一个子进程退出而给父进程发送退出码而父进程却并不接收是,此时子进程就会变成僵尸进程,僵尸进程会造成内存泄漏,僵尸进程并不能杀掉,因为从理论上讲此时的僵尸进程已经死了那么再去杀死它,可不没有意义吗?当时的做法时杀死父进程,子进程就会变成孤儿进程,然后让1号进程领养子进程,僵尸进程就会解除.可是,正确的做法应该是让父进程等待子进程,当子进程完成任务,提交退出码时父进程可以及时的接收它,那么子进程就可以正常退出了.
wait()函数:通常在父进程中使用,用来等待回收子进程的资源,从而防止僵尸进程的产生.如果父进程所有的子进程都在执行,没有终止,就会处于阻塞状态,等待执行结束的子进程.一旦有子进程结束,wait()就会返回.
敲黑板:wait()函数调用一次只能等待一个进程,若有多个子进程就要调用多个wait()函数来回收子进程.

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);   //同样也是输出型参数,获取子进程的退出状态,不关心则可以置为NULL.
若成功:返回被等待进程的pid,若失败,返回-1.

函数的参数是整形指针,用低两个2个字节来记录.
如果正常退出,则高8位用来记录进程正常退出时的退出码.
如果非正常退出,低8位用来记录非正常退出时接收到的信号.
status
可以通过代码来演示一下:

    #include<unistd.h>                                                                                                   
    2 #include<stdio.h>
    3 #include<sys/types.h>
    4 #include<sys/wait.h>
    5 int main()
    6 {
    7     pid_t pid = fork();
    8     if(pid == 0)
    9     {
   10         sleep(10);
   11         exit(10);
   12     }
   13     else if(pid > 0)
   14     {
   15         int status;
   16         int ret = wait(&status);
   17         if(ret > 0 && (status & 0x7F) == 0)   //正常退出的判断
   18         {
   19             printf("child exit num:%d\n",(status>>8)&0xFF);  
   20         }
   21         else if(ret > 0)//异常退出,可以在重新打开一个通道,来杀死子进程
   22         {
   23             printf("child code:%d\n",status&0xFF);
   24         }
   25     }
   26     else
   27     {
   28         perror("fork");
   29     }
   30       return 0;
   31 } 

再比如下面这个程序:

  1 #include<unistd.h>
  2 #include<stdio.h>
  3 #include<sys/types.h>
  4 #include<sys/wait.h>
  5 int main()
  6 {
  7     printf("父进程:%d\n",getpid());
  8     pid_t pid = fork();
  9     if(pid == 0)
 10     {
 11         //子进程
 12         sleep(3);
 13         printf("child1:%d\n",getpid());
 14     }
 15     else
 16     {
 17         pid = fork();
 18         printf("父进程在搬砖.\n");                                                                                     
 19         int status = 0;
 20         pid_t ret = wait(&status);//只调用了一次wait()函数
 21         printf("ret:%d\n",ret);
 22         if(pid == 0)                                                                                                   
 23         {
 24             printf("child2:%d\n",getpid());
 25         }   
 26         else
 27         {
 28             while(1)
 29             {
 30                 sleep(1);
 31             }   
 32         }   
 33     } 

上面这个程序只调用了一次wait()函数,所以第二次父进程产生的子进程就会变成僵尸进程.
运行结果
进程查看结果
所以要在程序中再调用一次wait()函数来杀死第二次父进程创建的子进程.
2次wait()的运行结果
进程查看
其中,在Linux中有提供特定的宏来处理判断正常退出和其退出码的.分别是:WIFEXITED(status)和WEXITSTATUS(status).
除了wait()函数,还有waitpid()函数也可以实现该功能.
waitpid()函数:

pid_t waitpid(pid_t pid,int* status,int options);
  • 当正常返回时,waitpid()就返回收集到的子进程的进程PID.
  • 如果设置了WNOHANG(就会按照非阻塞方式等待),而调用中的waitpid()发现没有已退出的子程序可以收集,就会返回0,不予以等待,就去执行父进程自己的事情;
  • 如果出错,就会返回-1,此时errno会打印出对应的错误.
  • options:默认为0.
    代码实现:
    1 #include<unistd.h>  
    2  #include<stdio.h>
    3 #include<sys/types.h>
    4 #include<sys/wait.h>
    5 int main()
    6 {
    7     pid_t pid = fork();
    8     if(pid == 0)
    9     {
   10         printf("child is running,pid is:%d\n",getpid());
   11         sleep(3);
   12         exit(257);
   13     }
   14     else if(pid < 0)
   15     {
   16         printf("error:%s\n",__FUNCTION__);
   17         return 1;
   18     }
   19     else
   20     {
   21         int status = 0;
   22         pid_t ret = waitpid(-1,&status,0);
   23         printf("wait\n");
   24         if(WIFEXITED(status) && ret == pid)
   25         {
   26             printf("wait child success,child code:%d\n",WEXITSTATUS(status));                                        
   27         }
   28         else
   29         {
   30             printf("failed.\n");
   31             return 1;
   32         }
   33     }
   34 }

这里写图片描述

进程程序替换

当进程调用exec()函数后,该进程执行的程序就会完全替代为一个新程序,而新程序从main()函数开始执行(程序的数据段,代码段,包括堆栈都会替换为新进程的).
注意注意注意:进程程序替换,替换的是子进程:程序替换,让子进程去替换其它的函数.目的:为了保护自己不受伤害,其它程序可能会破坏shell.所以很多命令都是让bash去执行的.
替换函数有一下6种:

#include<unistd.h>
int execl(const char* path,const char* arg,...);
int execlp(const char* file,const char* arg,...);
int execle(const char* path,const char* arg,...char* const env[]);
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[],char* const env[]);

上面6个函数特点:

  • 如果调用成功,就加载新的程序从启动代码开始执行,并且不返回值.(成功不返回)
  • 如果调用出错则返回-1.(所以此函数们只有在出错时才有返回值成功时没有返回值).
    上面的6个函数,我们依次进行模拟实现:
    6 //    int ret = execl("./a.out","a.out",NULL);                                                                       
    7 //    printf("ret:%d\n",ret);
    8 
    9 //    int ret = execlp("ls","ls","-l","-t","./",NULL);
   10 //    printf("ret:%d\n",ret);
   11 
   12 //    char* const envp[] = {"ls","-l","-t"};
   13 //    int ret = execle("/bin/ls","ls","-l","-t",NULL,envp);
   14 //    printf("ret:%d\n",ret);
   15 
   16 //    char* const argv[] = {"/bin/ps","-aux",NULL};
   17 //    int ret = execv("/bin/ps",argv);    
   18 //    printf("ret:%d\n",ret);
   19         
   20 //    char* const argv[] = {"ps","-aux",NULL};
   21 //    int ret = execvp("ps",argv);    
   22 //    printf("ret:%d\n",ret);
   23     
   24     char* const envp[] = {"ps",NULL};
   25     char* const argv[] = {"ls",NULL};
   26     int ret = execve("/bin/ps",argv,envp);
   27     printf("ret:%d\n",ret);

注意:参数envp和参数argv都是以NULL结束的.
在这6个函数中,它们都是以exec前缀开始的函数,那么后面的都怎么表示?

  • l(list):表示参数是命令行参数列表
  • v(vector):向量.表示参数采用的是数组.
  • p(path):表示可以自动搜索环境变量PATH.
  • e(envp):表示自己维护环境变量.
    通过这些的学习,现在呢,我们就可以写一个简单的shell了.
    后面添加代码.

猜你喜欢

转载自blog.csdn.net/yinghuhu333333/article/details/80206415