进程的操作(创建,终止,等待,替换)

目录

一.进程创建

1.初始fork函数

2.执行fork系统做了什么

3.fork返回值

4.写时拷贝

4.1 写时拷贝概念

4.2 为什么要有写时拷贝

5. fork常规用法

6. fork失败的原因

7. 总结

二.进程终止

1. 进程退出的场景

2. 进程常见的退出方法

3.exit和return的区别

4. _exit和exit区别

4.1 发现问题

4.2 结论

4.3 解决问题 

三.进程等待

1. 进程等待的必要性

2. 进程等待方法

2.1 wait()

2.2 waitpid()

 3. 获取子进程状态,int *status

 4. 总结

四.进程程序替换

1. 替换原理

2.替换函数

3.函数解释

 4. 命名理解


一.进程创建

1.初始fork函数

在Linux中fork函数是一个非常重要的函数,它从一个已存在的进程中创建一个新的进程。新进程为子进程,原进程为父进程。

#include<unistd.h>//头文件

pid_t fork(void);

2.执行fork系统做了什么

fork是一个系统调用,由操作系统的内核执行来创建一个进程。那内核创建进程做了些什么事呢?

  • 分配新的内存块和内核数据结构给子进程。分配内存和数据结构PCB,进程地址空间和页表等给子进程。
  • 将父进程部分数据结构内容拷贝到子进程。子进程一父进程为"模板"的,但是并不是完全一样的,例如pid等。
  • 添加子进程到系统进程列表中
  • fork返回,开始由调度器调度

3.fork返回值

fork返回值。

  • fork给子进程返回0
  • fork给父进程返回子进程pid
  • fork出错时返回-1

为什么fork由两个返回值?

        内核执行fork函数,申请内存,构建数据结构PCB,进程地址空间和页表等,放到调度列表中。

        fork是一个函数,里面含有创建进程的代码,例如:

        pid_t fork{
                //创建进程代码

                //最后return 

                return   pid;

        }

        在返回前已经将子进程创建好了,子进程也要执行return这条语句。原来进程父进程本来就要执行return语句。所以会有两个返回值。

为什么fork给子进程返回0,给父进程返回子进程的pid?

        因为父进程只有一个,子进程可以有多个,为了让父进程找到子进程(为后面等待做准备),所以给父进程返回子进程pid。

当fork被调用后,就有两个二进制代码相同的进程。而且他们运行到相同的地方。但是每个进程都将可以开始他们自己的旅程,看下面的代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main(){
  6   printf("Now pid :%d\n",getpid());
  7   pid_t id=fork();
  8   if(id<0){
  9     perror("fork error");
 10     return 1;
 11   }
 12   else if(id==0){
 13     //child
 14     printf("child pid :%d return :%d\n",getpid(),id);                                                                                                    
 15     sleep(2);
 16 
 17   }
 18   else{
 19     //father
 20     printf("father pid :%d return :%d\n",getpid(),id);
 21     sleep(2);
 22   }
 23 
 24   return 0;
 25 }

这里看到了三行输出,一行是fork前的语句,两行是fork后的语句。

为什么会出现这样的现象?

        父进程先先执行自己的代码,打印了fork前的语句,但执行到fork时,创建了一个新进程。此时有两个进程,父进程和子进程。而父子进程代码是共享的,但是数据是私有的。父进程返回值为子进程pid大于0,执行father pid...这条语句,子进程返回0,执行child pid...语句。

为什么子进程没有打印Now pid...这条语句?

        当父进程运行到fork语句时,进程控制块PCB里的程序计数器记录的下一次代码执行位置。而子进程的数据结构是以父进程为模板的,PCB里的程序计数器记录的也是同样的位置。所以不会从头开始执行。

画个图帮助理解:

 注意:fork后父子进程谁先执行是不确定的,由调度器决定。

4.写时拷贝

        通常父进程创建子进程时,父子进程代码是共享的,数据在都不写入时也是共享的,但是当一方试图写入时,数据就变成私有的了。

代码共享,子进程代码不仅只含有父进程fork后的代码,父进程fork前的代码也有。只是因为PCB程序计数器的原因,子进程从fork后开始执行。

为什么代码共享?

        代码是不可以修改的,是只读的(页表权限限制)。反正父子进程都不能修改代码,如果各自私有一份浪费空间。父子进程分流后代码仍然是共享的(一样),只是执不执行的问题。

        共享的意思是:两进程PCB中保存代码的地址相同,也就是父子进程"代码指针"指向同一个地址空间。不共享并不一定是虚拟地址变了,可能是实际物理地址变了,重新在实际内存中开辟一空间,改变页表中的映射关系。

为什么数据私有?

        因为进程之间具有独立性。

4.1 写时拷贝概念

        一开始父进程创建子进程代码和数据都是共享的,但是当父子进程一方要写入数据时,系统会在内存开辟另外一空间,将该数据拷贝过去,然后在新开辟空间写入数据。就是拷贝不是立马做的,而是需要写的时候再开辟空间,拷贝数据,写入数据。

4.2 为什么要有写时拷贝

        创建进程数据是有很多的,但是并不是所有的数据都立马要使用,并且不是所有的数据都要使用。但是如果立马将数据私有,就要将数据立马全部拷贝。把本来可以后面拷贝的,甚至不需要拷贝的数据都拷贝了。这样创建一个进程就比较浪费时间和空间。

 写时拷贝拷贝数据时,是将数据全部拷贝还是只将写的部分数据拷贝?

        只将写的数据拷贝。子进程也有一个新的进程地址空间(mm_struct)和页表。写时拷贝只将写的数据放到重新在内存开辟的空间,改变页表映射关系对应物理地址,虚拟地址没变。

5. fork常规用法

  • 一个父进程希望复制自己,使父子进程执行不同的代码段。上面的代码就是这样的。
  • 一个进程要执行不同的程序。后面讲进程替换的时候讲解。

6. fork失败的原因

  • 系统已经有太多的进程
  • 每个用户的进程数都有一个限制,进程数超过限制也会失败

7. 总结

        如何理解fork子进程的创建?

        本质上是系统多了一个进程。子进程以父进程为"模板"(并不是全部一样,例如pid)。创建对应数据结构(PCB,mm_struct,页表等)和代码数据。一开始复制进程代码和数据共享。当有一方写入数据时,给进程发生写时拷贝,将要写的数据重新开辟空间,修改页表的映射关系。

二.进程终止

1. 进程退出的场景

  • 代码正常运行完毕,结果也正确
  • 代码正常运行完毕,结果不正确
  • 代码运行异常

代码运行是以进程为载体的。

怎样才能知道代码运行退出情况呢?

        代码运行完毕后会有相应的退出码和退出信号。

2. 进程常见的退出方法

正常终止:(可以通过echo $? 来查看进程退出码)

  • 从main函数return返回。
return n//n就代表了退出码
  • 调用exit
#include<unistd.h>//头文件
void exit(int status)//status 退出码
  • 调用_exit
#include<unistd.h>//头文件
void _exit(int status)//status 退出码

说明:_exit(int status),虽然status是int,但是只有低8位可以被父进程所用。所以_exit(-1),执行echo $?时返回码时255. 

异常退出:

  • ctrl + c 信号终止

3.exit和return的区别

exit是终止整个进程,任何地方调用都会终止整个进程。

return是终止函数,只是在main函数里return n,等同于exit(n)。因为调用main函数的函数将main函数的返回值当作了exit的参数。

4. _exit和exit区别

4.1 发现问题

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main(){
  6   printf("hello world");

  7   return 0;                                                                                                                                              
  8 }

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main(){
  6   printf("hello world");
  7   exit(0);

  8 }

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main(){
  6   printf("hello world");
  7   _exit(0);                                                                                                                                              

  8 }

 从上面现象,我们发现用return和exit终止进程执行了语句,用_exit终止进程没有执行语句。有return和exit的区别可知,return结束进程也是调用了exit。

4.2 结论

_exit只是退出了进程。exit在调用时不仅仅终止了进程,还做了一些其它的事:

  • 执行用户通过atexit和on_exit定义的清理函数
  • 关闭所有打开的流,刷新缓存
  • 调用_exit,退出进程

4.3 解决问题 

        上面的问题,打印的字符串保存在缓冲区里。用_exit直接就退出了进程,没有刷新缓冲区。return退出进程实际上也是调用exit,exit退出进程会执行清理函数和刷新缓冲区,所以打印了语句。

三.进程等待

1. 进程等待的必要性

  • 子进程退出,父进程没有退出,子进程会变成僵尸进程。如果父进程不管子进程,会导致子进程一直在那里,也无法杀死该进程,因为这个进程已经退出了。这样会导致内存泄漏。
  • 父进程派给子进程的任务完成的怎么样,父进程需要知道。即父进程需要知道子进程退出的状态。
  • 父进程通过进程等待的方式,回收子进程的资源,获取子进程退出信息。

2. 进程等待方法

2.1 wait()

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status)

返回值:成功,返回被等待进程pid,失败,返回-1
参数:输出型参数,获取子进程退出状态,不关心状态可设置为NULL
    1 #include<stdio.h>
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5 
    6 int main(){
E>  7   pid_t id=fork();
    8   if(id<0){
    9     perror("fork error");
   10     return 1;
   11   }
   12   else if(id==0){
   13     int count=0;
   14     while(count<3){
   15       printf("child...,count:%d\n",count);
   16       count++;
E> 17       sleep(1);
   18     }
   19     exit(0);
   20   }
   21   else{
   22     int count=0;
   23     while(1){
   24       printf("father...,count:%d\n",count);                                                                                                              
   25       if(count==5){
   26         wait(NULL);
   27       }
   28       count++;
E> 29       sleep(1);
   30 
   31     }
   32   }
   33 
   34   return 0;
   35 }

 代码:设置子进程3秒后退出变成僵尸进程,父进程5秒后执行wait命令。查看子进程状态。

 上面验证了wait命令可以回收子进程的资源,防止内存泄漏。

注意:wait和waitpid是系统调用,所以资源回收是系统做的,父进程只是调用了系统接口。

    1 #include<stdio.h>
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5 
    6 int main(){
E>  7   pid_t id=fork();
    8   if(id<0){
    9     perror("fork error");
   10     return 1;
   11   }
   12   else if(id==0){
   13     int count=0;
   14     while(count<3){
   15       printf("child...,count:%d\n",count);
   16       count++;
E> 17       sleep(1);
   18     }
   19     exit(0);
   20   }
   21   printf("father before....\n");
   22   wait(NULL);                                                                                                                                          
   23   printf("father after...\n");
   24   return 0;
   25 }

这个代码证明父进程执行wait,父进程处于阻塞等待,一直在wait处等待子进程退出,再执行后面的代码。

2.2 waitpid()

#include<sys/wait.h>

pid_t waitpid(pid_t pid,int *status,int options)

返回值:
    正常返回时,返回子进程pid
    调用错误,返回-1.
参数:
    pid:
        pid=-1,等待任意一个子进程,与wait等效
        pid>0,等待进程id与pid相等的子进程。
    status:
        返回状态,为一个整形指针,用来接收返回值状态,后面介绍
    options:等待方式
        WNOHANG:如果pid指定进程没有结束,则waitpid函数返回0,不再等待。如果子进程正常结束,返回子进程id。
        0:以默认阻塞方式等待
    1 #include<stdio.h>
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5 
    6 int main(){
E>  7   pid_t id=fork();
    8   if(id<0){
    9     perror("fork error");
   10     return 1;
   11   }
   12   else if(id==0){
   13     int count=0;
   14     while(count<3){
   15       printf("child...,count:%d\n",count);
   16       count++;
E> 17       sleep(1);
   18     }
   19     exit(0);
   20   }
   21   printf("father before....\n");
   22   //wait(NULL);
   23   //和wait效果一样
   24   waitpid(-1,NULL,0);                                                                                                                                  
   25   printf("father after...\n");
   26   return 0;
   27 }

 3. 获取子进程状态,int *status

  • wait和waitpid,都有一个参数status参数(是一个整形指针),该参数是一个输出型参数,有参数系统填充(赋值)。

输出型参数时用来获取结果的,

输入性参数是用来提供参数的,已知值。

  • 如果传递NULL,表示不关心子进程的退出状态
  • 不为NULL,操作系统会根据该参数,将子进程退出信息填充到该参数,返回给父进程。
  • status不能简单当作int看待,需要当作位图看待。具体细节如下图(只研究status的低16位)

#include<stdio.h>                                                                                                                                        
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5 
    6 int main(){
E>  7   pid_t id=fork();
    8   if(id<0){
    9     perror("fork error");
   10     return 1;
   11   }
   12   else if(id==0){
   13     printf("child...\n");
   14 
E> 15     sleep(2);
W> 16     int x=1/0;//异常退出
   17     //exit(0);//正常退出,结果正确
   18     exit(10);//正常退出,结果不正确
   19   }
   20   int st=0;//接收退出状态
   21   pid_t res=waitpid(id,&st,0);//等待子进程,阻塞方式等待
   22   if(res>0){
   23     int tmp=st&0x7f;
   24     if(tmp){
   25       //异常退出
   26       printf("error exit,exit signal is %d\n",tmp);
   27     }
   28     else{
   29       //正常退出
   30       int code=(st>>8)&0xff;
   31       if(code){
   32         printf("result is error,code id %d\n",code);
   33       }
   34       else{
   35         printf("result id right,code is %d\n",code);
   36       }
   37     }
   38   }
   39   else{
   40     //调用错误
   41     perror("waitpid error");
   42     return 1;
   43   }
   44   return 0;
   45 }

 两种可以直接获取退出信息的宏:

  • WIFEXITED(status):若为正常终止子进程返回真。(查看进程是否正常退出)
  • WEXITSTATUS(status):若 WIFEXITED(status)为正(正常退出),获取退出码。(查看进程退出码)

进程阻塞式等待实现

#include<stdio.h>
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5 
    6 int main(){
E>  7   pid_t id=fork();
    8   if(id<0){
    9     perror("fork error");
   10     return 1;
   11   }
   12   else if(id==0){
   13     printf("child...\n");
   14 
E> 15     sleep(2);
W> 16     int x=1/0;//异常退出                                                                                                                               
   17     //exit(0);//正常退出,结果正确
   18     exit(10);//正常退出,结果不正确
   19   }
   20   int st=0;//接收退出状态
   21   pid_t res=waitpid(id,&st,0);//等待子进程,阻塞方式等待
   22   if(res>0){
   23     int tmp=st&0x7f;
   24     if(!WIFEXITED(st)){
   25       //异常退出
   26       printf("error exit,exit signal is %d\n",tmp);
   27     }
   28   else{
   29       //正常退出
   30       //int code=(st>>8)&0xff;
   31       int code=WEXITSTATUS(st);
   32       
   33       if(code){
   34         printf("result is error,code id %d\n",code);
   35       }
   36       else{
   37         printf("result id right,code is %d\n",code);
   38       }
   39     }
   40   }
   41   else{
   42     //调用错误
   43     perror("waitpid error");
   44     return 1;
   45   }
   46   return 0;
   47 }

 进程非阻塞式等待实现

#include<stdio.h>                                                                                                                                      
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<stdlib.h>
    5                          
    6 int main(){  
E>  7   pid_t id=fork();
    8   if(id<0){      
    9     perror("fork error");
   10     return 1;
   11   }          
   12   else if(id==0){       
   13     printf("child...\n");         
   14                                    
E> 15     sleep(5);
W> 16     int x=1/0;//异常退出 
   17     //exit(0);//正常退出,结果正确
   18     exit(10);//正常退出,结果不正确
   19   }
   20   int st=0;//接收退出状态                                 
   21   pid_t res=0;
   22   //一直循环,等子进程退出     
   23   do{        
   24     res=waitpid(id,&st,WNOHANG);//等待子进程,非阻塞方式等待
   25     if(res==0)
   26       printf("child is run\n");
E> 27     sleep(1);          
   28   }while(res==0);
   29   if(res>0){
   30     int tmp=st&0x7f;
   31     if(!WIFEXITED(st)){
   32       //异常退出
   33       printf("error exit,exit signal is %d\n",tmp);
   34     }
   35     else{
   36       //正常退出
   37       //int code=(st>>8)&0xff;
   38       int code=WEXITSTATUS(st);
   39       
   40       if(code){
   41         printf("result is error,code id %d\n",code);
   42       }
   43       else{
   44         printf("result id right,code is %d\n",code);
   45       }
   46     }
   47   }
   48   else{
   49     //调用错误
   50     perror("waitpid error");
   51     return 1;
   52   }
   53   return 0;
   54 }

 4. 总结

进程等待是什么?

        父进程通过wait/waitpid系统调用,等待子进程状态的一种现象。

为什么要进程等待?

        1.防止子进程僵尸状态,导致内存泄漏

        2.获得子进程退出结果

怎么进程等待?

        wait/waitpid

四.进程程序替换

fork创建一个子进程作用:

  1. 子进程执行父进程的部分代码,分流。
  2. 子进程与父进程执行完全不同的代码。程序替换。

1. 替换原理

        磁盘中保存一程序的代码和数据,程序替换就是,将磁盘中保存的新程序的代码和数据替换进程中的程序和代码。从新程序代码开始执行。

注意:程序替换没有创建新进程,所以该进程的pid并没有改变。

2.替换函数

#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 envp[])
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 envp[])

3.函数解释

  • 这些函数如果调用成功则加载到新的程序从启动代码开始执行,不再返回。

原因:执行第一条语句时,还没有替换。程序替换后(execl),将当前进程代码全部替换,也就是执行的是ls程序,后面的不执行了。 

  • 如果调用出错返回-1。
  • 所以exec函数只有出错返回值,没有成功返回值。 

exec函数错误时返回-1,不会发生替换,会继续执行下面的代码。

 4. 命名理解

exec函数看起来不叫容易搞混,但是掌握规律就很好记了。

  • l(list):表示采用列表形式。参数是一个一个传的。
  • v(vector):参数用数组传入
  • p(path):有p的exec函数会自动搜索环境遍历PATH。
  • e(env):表示自己维护环境变量。要自己用数组定义环境变量。

对照下面举列,就很清楚了:

 注意:含有l的exec函数最后或者envp前要以NULL结尾

用自己编写的代码替换当前进程

 环境变量:

 总结函数:

函数名参数格式是否要写路径是否使用当前环境变量
execl列表要写绝对路径使用系统环境变量
execlp列表不要写绝对路径使用系统环境变量
execle列表要写绝对路径使用自定义环境变量
execv数组要写绝对路径使用系统环境变量
execp数组不要写绝对路径使用系统环境变量
execv数组要写绝对路径使用自定义环境变量

 事实上,只有execve是真正的系统调用,其余5个函数最终都调用execve。

 ps说明:

  • 一般exec函数我们不会自己调用,一般我们会fork一个子程序,让子程序调用(去完成其它的任务),父进程只要wait就好了。
  • 父进程创建子进程后子进程执行替换函数后,父进程不受影响,因为父子进程之间具有独立性。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值