🥁作者: 华丞臧.
📕专栏:【LINUX】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站
文章目录
- 一、进程创建
- 1.1 fork函数
- 1.2 写时拷贝
- 1.3 补充
- 二、进程终止
- 2.1 进程退出场景
- 2.2 进程常见退出方法
- 2.3 _exit && exit
- 三、进程等待
- 3.1 进程等待必要性
- 3.2 进程等待方法
- 3.2.1 wait()系统调用
- 3.2.2 waitpid()系统调用
- 3.2.3 获取子进程status
- 正常终止
- 被信号所杀
- 3.3 进程等待的方式
一、进程创建
1.1 fork函数
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新的进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork();
//返回值:父进程返回子进程pid,子进程返回0
进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:
- 分配新的内存块和内核数据结构给子进程;
- 将父进程部分数据结构内容拷贝给子进程;
- 添加子进程到系统进程列表当中;
- fork返回,调度器开始调度。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main( void )
{
printf("Before: pid is %d\n", getpid());
pid_t id = fork();
if ( (id == -1)
perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
这里看到了三行输出,一行before,两行after。进程19913先打印before消息,然后它有打印after。另一个after
消息有19914打印的。注意到进程19914没有打印before,为什么呢?如下图所示:
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork函数返回值
- 子进程返回0。
- 父进程返回子进程pid。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程。
- 实际用户的进程数超过了限制。
1.2 写时拷贝
通常(一般情况),fork之后所有代码父子进程共享;此时父子进程不写入,数据也是共享的;当任意一方试图写入,这一方便以写时拷贝的方式拷贝一份。如下图:
为什么要写时拷贝?
- 父进程的数据,子进程不一定全用,即便会使用也不一定全部写入,因此可能会浪费内存;
- 最理想的情况,可以对会被父子进程修改的数据,进行分离拷贝,不需要修改的共享即可;但是从技术角度实现很复杂;
- fork的时候,对所有数据都进行拷贝会增加fork的成本(内存和时间上);
- 写时拷贝采用延迟拷贝策略,只有真正使用的时候才会分配给进程,变相提高了内存的使用率。
1.3 补充
CPU当中有一个eip程序计数器(寄存器),eip程序计数器会拷贝给子进程,子进程便从eip所指向的代码出开始执行。
eip程序计数器:保存当前正在执行指令的下一条指令。
为了保证进程的独立性,fork之后,操作系统创建子进程的内核数据结构(task_struct、mm_struct、页表),让子进程继承父进程的代码,通过写时拷贝的方式来进行数据的共享或者独立。
二、进程终止
2.1 进程退出场景
一个进程退出,一定是以下几种情况:
- 代码运行完毕,结果正确;
- 代码运行完毕,结果不正确;
- 代码异常终止。
2.2 进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 从main函数返回;
- 调用exit;
- _exit(系统调用接口);
异常退出:
- ctrl+c,信号终止。
2.3 _exit && exit
#include <unistd.h>
void _exit(int status);
参数:status 定义子进程的终止状态,父进程通过wait来获取该值
- 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
#include <unistd.h>
void exit(int status);
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数;
- 关闭所有打开的流,所有的缓存数据均被写入;
- 调用_exit。
exit和_exit的区别:
exit不仅仅会调用_exit,还会做其它的工作,使用exit函数终止进程会刷新缓冲区;
_exit是系统调用接口,使用_exit系统调用接口不会刷新缓冲区。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello Linux");
sleep(3);
exit(1);
//_exit(1);
}
return退出:
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返
回值当做 exit的参数。
三、进程等待
3.1 进程等待必要性
- 子进程退出,退出状态不被回收就会造成僵尸进程的问题,进而造成内存泄漏;
- 进程一旦进入僵尸进程,就无法被杀死了,因为谁也没办法杀死一个已经死去的进程;
- 父进程派给子进程的任务完成的如何,父进程需要知道;父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的目的:获取子进程的退出状态。
3.2 进程等待方法
3.2.1 wait()系统调用
wait()可以等待任意的一个退出的子进程,并回收该子进程,让子进程从Z
状态变为X
状态。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
//wait.cpp
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
//using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 3;
while(cnt--)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n", getpid(), getppid(), id);
sleep(1);
}
printf("子进程结束啦,父进程准备回收子进程!!!\n");
return -1;
}
else if(id > 0)
{
//parent
printf("这是父进程,pid:%d,ppid:%d,id:%d\n", getpid(), getppid(), id);
sleep(5);
int ret = wait(NULL); //父进程会停在这里等待读取子进程退出信息
printf("回收子进程成功,子进程状态status:%d\n", ret); //成功则返回子进程pid,失败返回-1
sleep(3);
}
return 0;
}
wait()的方案可以解决子进程Z状态,回收其资源,让子进程进入X状态,不过wait()功能较为简单不常用。
3.2.2 waitpid()系统调用
waitpid()可以等待特定的一个子进程的退出,并回收该子进程,让该进程从Z
状态变为X
状态。
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid
- pid < 0:等待任一个子进程。与wait等效。
- pid>0:等待其进程ID与pid相等的子进程。
status
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 0:阻塞等待。
//waitpid.cpp
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
//using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 3;
while(cnt--)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n", getpid(), getppid(), id);
sleep(1);
}
printf("子进程结束啦,父进程准备回收子进程!!!\n");
return 1;
}
else if(id > 0)
{
//parent
int status = 0;
printf("这是父进程,pid:%d,ppid:%d,id:%d\n", getpid(), getppid(), id);
sleep(10);
pid_t ret = waitpid(id, &status, 0); //父进程会停在这里等待读取子进程退出信息
printf("等待子进程成功, ret:%d, 子进程退出码为:%d\n", ret, (status >> 8) & 0xFF); //子进程正常运行结束
//printf("子进程终止信号为:%d\n", status & 0x7f); //子进程异常退出获取终止信号的方法
sleep(3);
}
return 0;
}
总结
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息;
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞;
- 如果不存在该子进程,则立即出错返回。
3.2.3 获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
正常终止
低八位为0,次低八位就是进程的退出状态,获取方法如下:
int status = 0;
pid_t ret = waitpid(id, &status, 0);
//status右移八位,再与上0xFF,即可获取其次低八位
printf("次低八位为:%d\n", (status >> 8) & 0xFF);
其中:
id
:子进程pid。status
:输出型参数。0
:表示waitpid()等待方式为阻塞等待。
被信号所杀
进程被信号所杀,输出的status参数低七位作为终止信号,第八位是core dump标志。
Linux中的信号我们已经见过了,如:
kill -9 [进程pid]
使用 kill
指令可以查看Linux下的所有信号,如下图:
一个进程正常运行结束是不会被信号杀死的;当一个进程运行出错时,操作系统会提供一个信号将进程杀死,当然用户也可以主动给进程一个信号将进程杀死。
进程被信号所杀死时,就说明该进程运行中出现异常,此时进程并没有运行到结束因此退出码没有意义;进程异常退出也是处于Z状态,父进程需要知道子进程为什么出现异常也需要获取子进程的终止状态。
int status = 0;
pid_t ret = waitpid(id, &status, 0);
printf("子进程终止信号为:%d\n", status & 0x7f);
3.3 进程等待的方式
进程等待分为两种方式:
阻塞等待
:如果子进程没有退出,父进程一直在等待;非阻塞等待
:如果子进程没有退出,父进程每过一段时间查看一次子进程是否退出。多次调用非阻塞接口,这个过程称为轮询检测。
如何理解父进程阻塞?
父进程的task_struct从运行队列放入到等待队列中,等待子进程退出。
进程等待的方式主要是在waitpid中使用:
pid_t waitpid(pid_t id, int *status, int options);
其中options
就是函数中可以用来设置等待方式的参数,options主要有以下两个常用的值:
- WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的pid。
- 0:若pid指定的子进程没有退出,则父进程停止在waitpid()函数处等待子进程退出;若子进程退出,则返回子进程pid。
WHOHANG的应用:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <vector>
typedef void (*handle_t)(); //函数指针类型重命名为 handle_t
//方法集
std::vector<handle_t> handles;
void fun1()
{
printf("我是fun1\n");
}
void fun2()
{
printf("我是fun2\n");
}
void Load()
{
//加载方法
handles.push_back(fun1);
handles.push_back(fun2);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n", getpid(), getppid(), id);
sleep(2);
}
return 1;
}
else if(id > 0)
{
//parent
int status = 0;
while(1)
{
//多次调用非阻塞接口读取子进程退出信息
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0)
{
//等待成功,子进程退出
printf("等待子进程成功, ret:%d, 子进程退出码为:%d,子进程终止信号为:%d\n", ret, (status >> 8) & 0xFF, status & 0x7f);
break;
}
else if (ret == 0)
{
//等待成功,子进程没有退出
if(handles.empty())
{
Load();
}
for(auto f : handles)
{
f(); //回调处理对应的任务
sleep(1);
}
}
else
{
//等待失败,暂不处理
}
}
}
return 0;
}
实验现象如下:
其中父进程通过轮询检测的方式读取子进程的退出信息。