网站备案号示例,惠州百度搜索排名优化,ssl 加密网站,dw怎么做网站标题图标文章目录 一、进程创建1.1 初识 fork 函数1.2 fork 函数返回值1.3 写时拷贝1.4 fork 的常规用法1.5 fork 调用失败的原因1.6 创建一批进程 二、进程终止2.1 进程退出场景2.2 strerror函数2.3 errno全局变量2.4 程序异常2.5 进程常见退出方法2.6 exit 函数2.7 _exit 函数和 exit… 文章目录 一、进程创建1.1 初识 fork 函数1.2 fork 函数返回值1.3 写时拷贝1.4 fork 的常规用法1.5 fork 调用失败的原因1.6 创建一批进程 二、进程终止2.1 进程退出场景2.2 strerror函数2.3 errno全局变量2.4 程序异常2.5 进程常见退出方法2.6 exit 函数2.7 _exit 函数和 exit 函数的区别 三、进程等待3.1 进程等待的必要性3.2 什么是进程等待3.3 进程等待具体是怎么做的3.3.1 wait方法3.3.2 waitpid方法3.3.3 父进程只等待一个进程阻塞式等待3.3.4 父进程等待多个子进程阻塞式等待 3.4 获取子进程的退出信息阻塞式等待3.5 wait、waitpid的实现原理3.6 非阻塞轮询等待 四、结语 一、进程创建
1.1 初识 fork 函数
在 Linux 中fork 函数用于从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。
#include unistd.h
pid_t fork(void); // fork 函数声明
返回值子进程中返回0父进程中返回子进程的 pid出错返回-1。一个进程调用 fork 函数后当控制转移到内核中的 fork 代码后执行 fork 函数的代码内核做了如下一些工作 分配新的内存块和内核数据结构给子进程。 将父进程部分数据结构内容拷贝到子进程中。 添加子进程到系统进程列表当中。 fork 返回开始调度器调度。
小Tips其实做完前两步子进程就已经被创建出来了。 当一个进程调用 fork 之后就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。说的再多还是需要通过代码来演示证明。
#include stdio.h
#include unistd.h int main()
{ printf(befor pid%d\n, getpid()); fork(); printf(after pid%d\n, getpid()); return 0;
}这里打印了三行输出一行是 befor 两行是 after。所以fork 之前父进程独立执行fork 之后父子两个执行流分别执行。注意fork 之后谁先执行完全由调度器决定。
1.2 fork 函数返回值 子进程返回0。 父进程中返回子进程的 pid出错返回-1。
关于 fork 函数的返回值问题在【Linux取经路】揭秘进程的父与子一文中已做了详细介绍其中包括“为什么给子进程返回0给父进程返回 pid”、“fork 函数是如何做到返回两次的”。感兴趣的小伙伴可以点回去看看。
1.3 写时拷贝
通常父子进程代码共享父子进程再不写入的时候数据也是共享的当任意一方试图写入便以写时拷贝的方式再生成一份。 操作系统是如何知道要进行写时拷贝的呢答案是父进程在创建子进程的时候操作系统会把父子进程页表中的数据项从读写权限设置成只读权限此后父进程和子进程谁要对数据进行写入就一定会触发权限方面的问题在进行权限审核的时候操作系统会识别出来历史上要访问的这个区域是可以被写入的只不过暂时是只读状态父子进程不管谁尝试对数据区进行写入的时候都会触发权限问题但是针对这这种情况操作系统并不做异常处理而是把数据拷贝一份谁写的就把页表项进行重新映射在拷贝完成后就把只读标签重新设置成可读可写。
操作系统为什么要采用写时拷贝呢父进程在创建子进程的时候单纯的从技术角度去考虑操作系统完全可以让父子进程共享同一份代码然后把父进程的多有数据全部给子进程拷贝一份技术上是完全可以实现的但是操作系统为什么没有这样干而是采用写时拷贝呢原因主要有以下几点首先假设父进程中国有100个数据子进程只需要对其中的一个进行修改剩下的99个子进程只读就可以那如果操作系统把这100个数据全给子进程拷贝了一份无疑是干了一件吃力不讨好的工作全部拷贝既浪费了时间又浪费的物理内存操作系统是绝对不会允许这种情况发生的因此对于数据段操作系统采用的是写时拷贝的策略。
1.4 fork 的常规用法 一个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端的请求生成子进程来处理请求。 一个进程要执行一个不同的程序。例如子进程从 fork 返回后调用 exec 函数。
1.5 fork 调用失败的原因 系统中有太多的进程。 实际用户的进程数超过了限制。
1.6 创建一批进程
通过 for 循环创建一批进程。
#include stdio.h
#include unistd.h
#include stdlib.h #define N 5 void func()
{ int cnt 10; while(cnt) { printf(I am chid, pid%d, ppid%d\n, getpid(), getppid()); cnt--; sleep(1); } return;
} int main()
{ int i 0; for(i 0; i N; i) { pid_t id fork(); if(id 0)// 只有子进程会进去 { func(); exit(0);// 子进程走到这里就退出了 } } sleep(1000); return 0;
}小Tips父进程执行的速度是很快的由于父进程的 for 循环里没有 sleep 函数所以五个子进程几乎是在同一时间被创建出来创建出来的每一个子进程会去调用 func 函数每一个子进程执行完 func 函数后会执行 exit 函数退出。父子进程谁先执行完全是由调度器来决定的。
二、进程终止
2.1 进程退出场景 代码运行完毕结果正确 代码运行完毕结果不正确 代码异常终止
一般代码运行完毕结果正确我们是不会关心代码为什么跑对了。但是当代码运行完毕结果不正确我们作为程序员是需要知道为什么结果不正确因此进程需要将运行结果以及不正确的原因告诉程序员。这就是 main 函数里常写的 return 0 的作用。return 后面跟的数字叫做进程的退出码表征进程的运行结果是否正确不同的返回数字表征不同的出错原因0表示 success。main 函数 return 的这个0最终会被父进程即 bash 拿到。可以在 bash 中输出 echo $? 指令查看上一个子进程的退出码。$? 表示命令行当中最近一个进程运行的退出码。
int main()
{ printf(模拟一段逻辑\n); return 0;
}小Tips对于一个进程一般而言只有父进程最关心它的运行情况
2.2 strerror函数
上面提到的退出码本质上是数字它更适合机器去查看作为程序员我们可能对数字没有那么敏感即可能不知道该数字表示的是什么意思。因此 strerror 函数的作用就是将一个退出码转换成为一个错误信息描述。可以通过下面这段代码来打印当前系统支持的所有错误码对应的错误信息。
int main()
{ int i 0; for(; i 200; i) { printf(%d, %s\n, i, strerror(i)); } return 0;
} 2.3 errno全局变量
errno 是 C 语言给我们提供的一个全局变量它里面保存的是最近一次执行的错误码何谓最近一次执行C 语言为我们提供了很多的库函数在调用这些库函数失败的时候C 语言就会将 errno 设置成对应的数字这个数字就表示调用该函数出错的错误码。
#include stdio.h
#include unistd.h
#include stdlib.h
#include string.h
#include errno.h int main()
{ int ret 0; char* str (char*)malloc(1000*1000*1000*4); if(str NULL) { printf(malloc error%d, %s\n, errno, strerror(errno)); ret errno; } else { printf(malloc success!\n); } return ret;
}2.4 程序异常
代码如果出现了异常本质上代码可能就没有跑完因此可能就没有执行 return 语句。所以程序如果出现了异常那么该程序的退出码是没有意义的。因此对于一个执行结束的进程来说我们要先看它是否出异常如果没有异常再去看它的退出码是否正确。对于异常我们也需要知道程序为什么异常以及发生了什么异常。进程出现异常本质上是我们的进程收到了对应的信号。像程序中除0空指针解引用一般都会引发硬件错误由我们的操作系统向对应的进程发送信号。Linux 系统的所有信号如下图所示。 int main()
{ char* pc NULL; *pc a; // 解引用空指针会发生段错误return 0;
}下面证明该异常是因为程序收到了对应的信号。 2.5 进程常见退出方法
正常终止指程序的代码执行完了结束而不是收到信号结束。 从 main 函数返回即 return 调用 exit 函数 _exit
异常退出。 ctrlc 信号终止
2.6 exit 函数
#include unistd.h
void exit(int status);在代码中的任何地方调用 exit 函数都表示调用进程直接退出。退出码就是 exit 函数的参数 status。说这个主要是为了区分 return 和 exitreturn 只有在主函数main中出现才表示进程退出在普通的函数中使用 return 仅表示函数返回而在函数中使用 exit也会让进程直接退出。
2.7 _exit 函数和 exit 函数的区别 上面的现象我们是可以理解的printf 函数后面没有加 \n因此要打印的内容先被保存在了缓冲区中等休眠两秒后程序执行 exit 退出程序退出会刷新缓冲区所以程序运行我们看到的效果是前两秒什么也没打印在程序退出前才执行了打印。下面我们把 exit 换成 _exit 再看看效果。 这次程序执行后等待了两秒直接退出了并没有将信息打印出来。
结论_exit 是系统调用exit 是库函数。exit 最后会调用 _exit但是在调用 _exit 之前还做了下面几个工作。 执行用户通过 atexit 或 on_exit 定义的清理函数。 关闭所有打开的流所有的缓冲区数据均被写入。 调用 _exit()。 小Tips通过上面的现象我们可以的出一个结论那就是缓冲区一定不在内核中而是在用户空间。因为如果在内核中那调用 _exit 函数的时候也必然会把缓冲区中的数据进行刷新如果不刷新那还维护这个缓冲区干嘛呢正是因为缓冲区在用户区_exit 作为系统调用看不到用户区的数据所以才没办法刷新。
三、进程等待
3.1 进程等待的必要性 在前面的文章中讲过子进程退出父进程如果不管不顾就可能造成“僵尸进程”的问题进而会造成内存泄露。 另外进程一旦变成僵尸状态那就刀枪不入“杀人不眨眼”的 kill -9 指令也无能为力因为谁也没有办法杀死一个已经死去的进程。 最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是不对或者是否正常退出。 父进程通过进程等待的方式回收子进程资源获取子进程的退出信息。
总结僵尸进程无法被杀死需要通过进程等待来杀掉它进而解决内存泄露的问题这是进程等待的必要性。其次通过进程等待让父进程获得子进程的退出情况看布置的任务完成的怎么样了这一点对父进程来说是可选项即父进程也可以选择不关心如果要关心了需要通过进程等待去获取。
3.2 什么是进程等待
进程等待就是在父进程的代码中通过系统调用 wait/waitpid来进行对子进程进行状态检测与回收的功能。
3.3 进程等待具体是怎么做的
3.3.1 wait方法
#include sys/types.h
#include sys/wait.hpid_t wait(int* status);返回值成功返回被等待进程的 pid失败返回-1。 参数输出型参数获取子进程的退出状态不关心则可以设置成为 NULL。
3.3.2 waitpid方法
#include sys/types.h
#include sys/wait.hpid_t waitpid(pid_t pid, int* status, int options);返回值当正常返回的时候 waitpid 返回等待到的子进程的进程 ID如果设置了选项 WNOHANG而调用的过程中没有子进程退出则返回0如果调用中出错则返回-1这时 errno 会被设置成相应的值以指示错误所在。 参数 pidpid -1 表示等待任意一个子进程。与 wait 等效pid 0 表示等待进程 ID 与 pid 相等的子进程。 参数 statusWIFEXITED(status)查看子进程是否正常退出。若为正常终止子进程返回的状态则为真WEXITSTATUS(status)查看进程的退出码。若非零提取子进程的退出码。 参数 options0表示父进程以阻塞的方式等待子进程即子进程如果处在其它状态不处在僵尸状态Z状态父进程会变成 S 状态操作系统会把父进程放到子进程 PCB 对象中维护的等待队列中以阻塞的方式等待子进程变成僵尸状态当子进程运行结束操作系统会检测到把父进程重新唤醒然后回收子进程WNOHANG非阻塞轮询等待若 pid 指定的子进程没有结束处于其它状态则 waitpid() 函数返回0不予等待。若正常结束则返回该子进程的 ID。
小Tipswait 和 waitpid 都只能等待该进程的子进程如果等待了其它的进程那么就会出错。
3.3.3 父进程只等待一个进程阻塞式等待
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/types.h
#include sys/wait.h int main()
{ pid_t id fork(); if(id 0) { perror(fork); return 1; } else if(id 0) { // child int cnt 5; while(cnt) { printf(I am child, pid%d, ppid%d, cnt%d\n, getpid(), getppid(), cnt--); sleep(1); } exit(0); } else { int cnt 10; // parent while(cnt) { printf(I am parent, pid%d, ppid%d, cnt%d\n, getpid(), getppid(), cnt--); sleep(1); } int ret wait(NULL); if(ret id) { printf(wait success!\n); } sleep(5); } return 0;
}结果分析前五秒父子进程同时运行紧接着子进程退出变成僵尸状态五秒钟后父进程对子进程进行了等待成功将子进程释放掉最后再五秒钟后父进程也退出整个程序执行结束。
3.3.4 父进程等待多个子进程阻塞式等待
一个 wait 只能等待任意一个子进程因此父进程如果要等待多个子进程可以通过循环来多次调用 wait 实现等待多个子进程。
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/types.h
#include sys/wait.h #define N 5
// 父进程等待多个子进程
void RunChild()
{ int cnt 5; while(cnt--) { printf(I am child, pid%d, ppid%d\n, getpid(), getppid()); sleep(1); } return;
}
int main()
{ for(int i 0; i N; i) { pid_t id fork();// 创建一批子进程 if(id 0) { // 子进程 RunChild(); exit(0); } // 父进程 printf(Creat process sucess%d\n, id); } sleep(10); for(int i 0; i N; i) { pid_t id wait(NULL); if(id 0) { printf(Wait process%d, success!\n, id); } } sleep(5); return 0;
}小Tips如果子进程不退出父进程在执行 wait 系统调用的时候也不返回默认情况默认叫做阻塞状态。由此可以看出一个进程不仅可以等待硬件资源也可以等待软件资源这里的子进程就是软件。
3.4 获取子进程的退出信息阻塞式等待
在 2.1 小结提到过进程有三种退出场景。正是因为有这三种退出场景父进程等待希望获得子进程退出的以下信息子进程代码是否异常没有异常结果对嘛不对是因为什么呢 子进程这些所有的退出信息都被保存在 status 参数里面。 wait 和 waitpid 都有一个 status 参数该参数是一个输出型参数由操作系统填充。 如果传递 NULL表示不关心子进程的退出状态信息。 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。 status 不能简单的当做整形来看待可以当做位图来看待具体细节如下图只需要关注 status 低16比特位 小Tips操作系统没有0号信号因此如果低七位是0说明子进程没有收到任何信号。
int main()
{pid_t id fork();if(id 0){perror(fork);return 1;}else if(id 0){// childint cnt 5, a 10;while(cnt){printf(I am child, pid%d, ppid%d, cnt%d\n, getpid(), getppid(), cnt--);sleep(1);a / 0; // 故意制造一个异常}exit(11); // 将退出码故意设置成11}else {// parentint cnt 10;while(cnt){printf(I am parent, pid%d, ppid%d, cnt%d\n, getpid(), getppid(), cnt--);sleep(1); }// 目前为止进程等待是必须的//int ret wait(NULL);int status 0;int ret waitpid(id, status, 0);if(ret id){// 获取子进程退出状态信息的关键代码// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00printf(wait success! exit signal%d, exit code%d!\n, status0X7F, (status 8)0XFF); }sleep(5);}return 0;
}小Tips通过运行结果可以看出子进程收到了8号信号子进程的退出码是0。代码中子进程的退出码被我们设置成了11这侧面印证了我们上面讲到的进程收到信号后被异常终止此时代码没有执行完毕所以此时进程的退出码是不可信的。
// 常规的进程等待代码
int status 0;
int ret waitpid(id, status, 0);
if(ret id)
{// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00//printf(wait success! exit signal%d, exit code%d!\n, status0X7F, (status 8)0XFF);if(WIFEXITED(status)){printf(子进程正常退出退出码是%d\n, WEXITSTATUS(status));}else {printf(子进程被异常终止\n);}
}3.5 wait、waitpid的实现原理
一个进程在退出后父进程回收之前它的代码和数据都被释放了但是它的 PCB 对象并没有被释放因为它收到的信号和退出码信息都保存在 PCB 对象中wait 和 waitpid 本质上就是操作系统去检查一个进程是否处于僵尸状态Z状态如果处于 Z 状态就去它的 PCB 对象中拿到该进程收到的信号和退出码信息再把这些信息赋值给 status然后将该进程的状态设置成 X。这个工作只能由操作系统来做因为 PCB 对象属于内核数据结构对象不允许用户直接访问。
3.6 非阻塞轮询等待
前面说过若父进程采用阻塞式等待如果子进程没有处于僵尸状态那么此时父进程处于阻塞状态什么也干不了。若父进程采用非阻塞轮询等待如果子进程没有处于僵尸状态那么父进程可以继续去干它的事情。
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/types.h
#include sys/wait.h// 父进程只等待一个子进程(非阻塞轮询等待)
int main()
{pid_t id fork();if(id 0){perror(fork);return 1;}else if(id 0){// childint cnt 5;while(cnt){printf(I am child, pid%d, ppid%d, cnt%d\n, getpid(), getppid(), cnt--);sleep(1);//a / 0;}exit(11);}else {// parent // 目前为止进程等待是必须的//int ret wait(NULL);while(1){int status 0;int ret waitpid(id, status, WNOHANG);if(ret 0){// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00//printf(wait success! exit signal%d, exit code%d!\n, status0X7F, (status 8)0XFF);if(WIFEXITED(status)){printf(子进程正常退出退出码是%d\n, WEXITSTATUS(status));}else {printf(子进程被异常终止\n);}break;}else if(ret 0){// 父进程的任务可以写在这里printf(child process is running...\n);}else{printf(等待出错\n);}sleep(1);}sleep(2);}return 0;
}小Tips在非阻塞轮询等待过程中父进程可以去执行自己的任务前提是该任务轻量化且可返回非阻塞轮询等待的核心任务还是回收子进程。子进程创建出来父子进程谁先执行是由调度器说了算进程等待在一定程度上确保了父进程一定是最后一个退出的这样可以避免子进程变为僵尸进程进而导致内存泄露的问题。
四、结语
今天的分享到这里就结束啦如果觉得文章还不错的话可以三连支持一下春人的主页还有很多有趣的文章欢迎小伙伴们前去点评您的支持就是春人前进的动力