目录
1.进程创建
1.1 fork函数初识
1.2 写时拷贝
1.3 fork常规用法
1.4 fork调用失败的原因
2.进程终止
2.1进程退出场景
2.2进程常见退出方法
3. 进程等待
3.1进程等待必要性
3.2进程等待的方法
3.2.1wait方法
3.2.1waitpid方法
3.3 获取子进程status
3.4 具体代码实现
3.4.1进程的阻塞等待方式
3.4.2进程的非阻塞等待方式:
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
——By 作者:新晓·故知
在 linux 中 fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。#include
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:int main( void ) { pid_t pid; printf("Before: pid is %d\n", getpid()); if ( (pid=fork()) == -1 )perror("fork()"),exit(1); printf("After:pid is %d, fork return %d\n", getpid(), pid); sleep(1); return 0; } 运行结果: [root@localhost linux]# ./a.out Before: pid is 43676 After:pid is 43676, fork return 43677 After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after 消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:![]()
#include
#include #include int main() { printf("我是一个进程:pid: %d, ppid: %d\n",getpid(),getppid()); fork(); printf("已调用fork()函数,我依旧是一个进程:pid: %d, ppid: %d\n",getpid(),getppid()); return 0; } fork之后,有两个进程,父子进程共享所有的代码!但子进程执行的后续代码!=共享的所有代码,只不过子进程只能从这里开始执行!可以通过vfork+return验证!进程=内核的进程数据结构+进程的代码和数据。fork之后,创建子进程的内核数据结构(struct task_struct+struct mm_struct + 页表)+代码继承父进程,数据以写时拷贝的方式,来进行共享!fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid。
1.2 写时拷贝
写时拷贝本身就是由OS的内存管理模块实现的!为什么要写时拷贝?创建子进程的时候,就把数据分开不行吗?这种方案是可以的,但是不选择这样做:因为父进程的数据子进程不一定全用,避免浪费空间!即便使用,也不一定全部写入!且最理想的情况:只有会被父子进程修改的数据进行分离拷贝,不需要修改的数据共享即可,但这从技术角度实现复杂!如果fork的时候,就直接拷贝数据给子进程,这会增加fork的成本(内存和时间)因此采用写时拷贝!写时拷贝的好处:只会拷贝父子进程修改的(拷贝数据的最小成本)。只读的数据共享!但拷贝的成本依旧存在!而写时拷贝本质上是延迟拷贝策略,只有真正使用的时候才传递(你想要资源,但是不立马使用,那就先不给你,那也就意味着可以先给别人,这样变相的提高资源利用率)。1.3 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
#include
#include #include int cnt=0; int main() { for(; ;) { pid_t id=fork(); if(id<0) { printf("创建子进程失败!已创建的子进程个数 cnt:%d\n",cnt); break; } if(id==0) { printf("我是一个子进程,我得pid:%d\n",getpid()); sleep(200); exit(0); } cnt++; //统计 父进程创建的子进程个数 } return 0; }
2.1进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.2进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):1. 从main返回2. 调用exit3. _exit异常退出:ctrl + c,信号终止_exit函数:#include
void _exit(int status); 参数:status 定义了进程的终止状态,父进程通过wait来获取该值说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。exit 函数:#include
void exit(int status); exit最后也会调用exit, 但在调用exit之前,还做了其他工作:1. 执行用户通过 atexit或on_exit定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写入3. 调用_exit
实例:
int main() { printf("hello"); exit(0); } 运行结果: [root@localhost linux]# ./a.out hello[root@localhost linux]# int main() { printf("hello"); _exit(0); } 运行结果: [root@localhost linux]# ./a.out [root@localhost linux]#
return 退出:return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。C/C++中main函数是程序的入口,那return 0是给谁return?为何是0?其他值可以吗?return是进程代码执行完,把结果是否做正确返回!(0:sucess,!0:fail)非零标识不同失败的原因!因此将return X称为进程退出码,表征进程退出信息!这个退出信息让父进程读取!(父进程创建子进程是要让子进程帮父进程做一些事情),因此return给了父进程!![]()
一般而言,失败的非零值该如何设置?以及默认表达的含义?
可以随便设置。但OS是如何处理呢?
错误退出码可以对应不同的错误原因,方便定位问题!
关于进程终止的常见做法:
1.在main函数中return,(非main函数中return不代表进程退出,只代表函数结束)
2.在自己的代码任意地点中,调用exit()函数。(或_exit()函数)
exit终止进程,会刷新缓冲区,_exit直接终止进程,不会刷新缓冲区!
![]()
关于进程终止,内核做了什么?
进程=内核结构+进程代码和数据。进程终止,内核会释放进程代码和数据,但可能并不会释放进程的内核数据结构!我们知道,创建对象:1.开辟空间2.初始化。而进程终止时,内核可能不会释放内核的数据结构,只是将其设置为无效,进行维护!当再次创建时,节省时间成本。这里维护与内核的数据结构缓冲池有关,(slab分派器)(多次高频度创建结构,就会考虑使用数据结构池)
exit与_exit举例:
3.1进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息3.2进程等待的方法
3.2.1wait方法
#include
#include pid_t wait(int*status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL 3.2.1waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
模拟进程等待:#include
#include #include #include #include int main() { pid_t id=fork(); if(id==0) { //id==0:子进程 while(1) { printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); } } else { //id>0:父进程 printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); printf("我准备等待子进程\n"); sleep(40); pid_t ret=wait(NULL); if(ret<0) { printf("等待失败!\n"); } else { printf("等待成功! result: %d\n",ret); } sleep(20); } return 0; } pid_t wait(int *status);
wait等待任意一个退出的子进程!
pid_t waitpid (pid_t pid, int *status, int options);
返回值pid_t >0:等待子进程成功,返回值就是子进程的pid
返回值pid_t <0:等待失败
pid>0:pid是几,就代表等待哪一个子进程
pid = -1:等待任意进程!
options =0:阻塞等待
status:指针参数,是一个输出型参数:通过调用该函数,从函数内部拿出来特定的数据!
其实就是从操作系统拿出来!
当父进程等待子进程时:
1.子进程执行完正常退出后,子进程会将自己的退出信息写入自己的task_struct结构,父进程调用wait/waitpid()会从操作系统找到子进程的task_struct获取退出信息(从子进程的进程控制块中获取)。
2.子进程没有正常执行完(没有退出),那么父进程就必须阻塞等待,等子进程退出后,才能正式拿出来。
注:
wait/waitpid()是系统调用!
关于status:只需要关心该整数的低16bit位!(32bit /整数) 这16个bit位被分为3部分。
#include
#include #include #include #include int main() { pid_t id=fork(); if(id==0) { //id==0:子进程 int cnt=5; while(1) { printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); cnt--; if(!cnt) { break; } } exit(23); } else { //id>0:父进程 printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); printf("我准备等待子进程\n"); int status=0; pid_t ret=waitpid(id,&status,0); if(ret>0) { printf("等待成功!ret: %d,我所等待的子进程的退出码:%d \n",ret,(status>>8)&0xFF); //取出status的低16位 } // sleep(40); // pid_t ret=wait(NULL); // if(ret<0) // { // printf("等待失败!\n"); // } // else // { // printf("等待成功! result: %d\n",ret); // } // sleep(20); } return 0; } status的低8位:(1~8bit位)
1~7bit:(终止信号)
第8bit位:code dump
#include
#include #include #include #include int main() { pid_t id=fork(); if(id==0) { //id==0:子进程 //int cnt=5; while(1) { printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); // cnt--; // if(!cnt) // { // break; // } } exit(0); } else { //id>0:父进程 printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); printf("我准备等待子进程\n"); int status=0; pid_t ret=waitpid(id,&status,0); if(ret>0) { printf("等待成功!ret: %d,我所等待的子进程的退出码:%d ,退出信号:%d\n", ret,(status>>8)&0xFF,status&0x7F); //取出status的低16位,以及低16位的前7位(终止信号) } // sleep(40); // pid_t ret=wait(NULL); // if(ret<0) // { // printf("等待失败!\n"); // } // else // { // printf("等待成功! result: %d\n",ret); // } // sleep(20); } return 0; } 结论:
代码运行完毕:退出码:exit()=0
反馈结果:看退出信号
异常终止:看退出信号
一旦进程出现异常,退出码就没有意义,只看退出信号!
可以使用Linux提供的宏查看:
#include
#include #include #include #include int main() { pid_t id=fork(); if(id==0) { //id==0:子进程 int cnt=5; while(1) { printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); sleep(1); cnt--; if(!cnt) { break; } } exit(0); } else { //id>0:父进程 printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid()); printf("我准备等待子进程\n"); int status=0; pid_t ret=waitpid(id,&status,0); if(ret>0) { if(WIFEXITED(status)) { printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status)); printf("等待成功!ret: %d,我所等待的子进程的退出码:%d ,退出信号:%d\n", ret,(status>>8)&0xFF,status&0x7F); //取出status的低16位,以及低16位的前7位(终止信号) } } // sleep(40); // pid_t ret=wait(NULL); // if(ret<0) // { // printf("等待失败!\n"); // } // else // { // printf("等待成功! result: %d\n",ret); // } // sleep(20); } return 0; } 3.3 获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):![]()
测试代码: #include
#include #include #include #include int main( void ) { pid_t pid; if ( (pid=fork()) == -1 ) perror("fork"),exit(1); if ( pid == 0 ){ sleep(20); exit(10); } else { int st; int ret = wait(&st); if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出 printf("child exit code:%d\n", (st>>8)&0XFF); } else if( ret > 0 ) { // 异常退出 printf("sig code : %d\n", st&0X7F ); } } } 测试结果: [root@localhost linux]# ./a.out #等20秒退出 child exit code:10 [root@localhost linux]# ./a.out #在其他终端kill掉 sig code : 9 3.4 具体代码实现
当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待。本质:就是当前进程自己变成阻塞状态,等条件(任意的软硬件)就绪的时候,再被唤醒!3.4.1进程的阻塞等待方式
int main() { pid_t pid; pid = fork(); if(pid < 0){ printf("%s fork error\n",__FUNCTION__); return 1; } else if( pid == 0 ){ //child printf("child is run, pid is : %d\n",getpid()); sleep(5); exit(257); } else{ int status = 0; pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S printf("this is test for wait\n"); if( WIFEXITED(status) && ret == pid ){ printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); }else{ printf("wait child failed, return.\n"); return 1; } } return 0; } 运行结果: [root@localhost linux]# ./a.out child is run, pid is : 45110 this is test for wait wait child 5s success, child return code is :1.
3.4.2进程的非阻塞等待方式:
#include
#include #include #include int main() { pid_t pid; pid = fork(); if(pid < 0){ printf("%s fork error\n",__FUNCTION__); return 1; }else if( pid == 0 ){ //child printf("child is run, pid is : %d\n",getpid()); sleep(5); exit(1); } else{ int status = 0; pid_t ret = 0; do { ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待 if( ret == 0 ){ printf("child is running\n"); } sleep(1); }while(ret == 0); if( WIFEXITED(status) && ret == pid ){ printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); }else{ printf("wait child failed, return.\n"); return 1; } } return 0; }
#include
2 #include 3 #include 4 #include 5 #include 6 7 int main() 8 { 9 pid_t id = fork(); 10 if(id==0) 11 { 12 //子进程 13 while(1) 14 { 15 printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid()); 16 sleep(5); 17 int *p=NULL; 18 *p=100; //野指针 19 // break; 20 } 21 exit(100); 22 } 23 else if(id>0) 24 { 25 //父进程 26 printf("我是父进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid()); 27 int status=0; 28 pid_t ret=waitpid(-1,&status,0); 29 if(ret>0) 30 { 31 printf("等待成功!我等待的返回值:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF); 32 } 33 sleep(3); 34 } 35 } 非阻塞:
#include
#include #include #include #include int main() { pid_t id = fork(); if(id==0) { //子进程 while(1) { printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid()); sleep(5); // int *p=NULL; // *p=100; //野指针 // break; } exit(100); } else if(id>0) { //父进程 //基于非阻塞的轮巡检测方案 int status=0; while(1) { pid_t ret=waitpid(-1,&status,WNOHANG); if(ret>0) { printf("等待成功,返回值为:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF); break; } else if(ret==0) { //等待成功了,但是子进程没有退出 printf("我是父进程,等待子进程,奥,还没,那么我父进程做其他事情...\n"); sleep(1); } } // printf("我是父进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid()); // int status=0; // pid_t ret=waitpid(-1,&status,0); // if(ret>0) // { // printf("等待成功!我等待的返回值:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF); // } // sleep(3); } } 使用C++演示,父进程回调处理对应的任务: