目录
创建进程[1~4]
1.写时拷贝
写时拷贝:父子代码共享,父子不写入时,数据也是共享的;当任意一方试图写入,就会以写时拷贝的方式各自一份副本。
写时拷贝是一种按需申请资源的策略!
2.fork函数
从已经存在的进程中创建一个新的进程,新进程为子进程,原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0;父进程返回子进程id;出错时返回-1;
当调用fork时,内核所做的有:
分配新的内存块与内核数据结构给子进程;
将父进程的部分内核数据结构拷贝给子进程;
将子进程添加到系统的进程列表中;
fork返回,开始调度;
3.fork创建子进程的目的(用法)
- 希望子进程会执行父进程的一部分代码;
- 希望子进程会执行一个全新的程序;
一个进程被fork创建出来,不是用命令行的方式加载到内存的,所以fork出来的子进程理论是没有代码和数据的,所以fork出来的子进程要么从父进程继承程序,要么就自己重新加载一个新的程序。
4.fork失败的原因
- 系统中有太多的进程,资源不足,创建失败;
- OS会限制普通用户创建进程的数量,存在上限,太多会创建失败。
进程终止[5~7]
5.进程终止的情况
衡量进程的退出结果是通过:信号+退出码的方案。
编写代码的时候,在main函数的最后都会return 0,那么为什么要return 0呢?
——return 0;返回值0就是退出码,进程退出码用来表征在进程执行结束时,结果是否正确;
如果结果正确,则返回0;
如果结果错误,则返回非0;
int main()
{
//...
return 0;//进程退出码
}
当进程执行结果不正确,我们关心的是为什么不正确,所以结果错误时,返回值“非0”有很多(1, 2, 3…),通过不同的返回值,来表示不同的错误原因。
可以通过
echo $?
来获取退出码;
注:只会保留最近一次执行进程的退出码,所以首次运行echo $?的时候获取的是你自己进程的退出码,但是后续再次获取的就是上次echo $?的退出码了。
C提供的退出码获取
退出码可以自定义,同时C语言也提供了默认的退出码:
头文件:string.h
退出码获取接口:char *strerror(int errnum);
编写一个程序获取退出码:
#include <stdio.h>
#include <string.h>
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d : %s\n", i, strerror(i));
}
}
获取结果:
可以观察到C语言一共提供了133个退出码;
注意:不是所有程序都要遵守C语言的退出码,可以自己定义。
6.进程退出的理解
OS少了一个进程,OS就要释放进程对应的内核数据结构+代码和数据。
7.进程退出的方法
mian函数return退出
exit函数退出
头文件:unistd.h
函数:void exit(int status);
注:函数参数就是进程退出码,等价于main函数return x;
_exit函数退出
头文件:unistd.h
函数:void _exit(int status);
注:使用时看似与exit没有区别,但是实际不同,并且推荐使用exit。
区别:
exit会刷新缓冲区数据;
_exit不会做任何处理,直接退出;
例如:
下面的两个程序,第一个退出时会刷新缓冲区中的Hello world!打印到屏幕;第二个则不会打印直接退出。
int main()
{
printf("Hello world!");
sleep(1);
exit(1);
}
----------------------------
int main()
{
printf("Hello world!");
sleep(1);
_exit(1);
}
进程等待[8~10]
1.为什么要进程等待?
——回收子进程,避免内存泄漏;获取子进程执行的结果(退出信息);
(子进程的执行结果:退出码(代码正常执行结束) + 信号(代码运行异常))
2.什么是进程等待?
——通过系统调用,获取子进程退出码或退出信号+释放内存。
3.如何进程等待?
——wait/waitpid;
8.wait
作用:等待子进程状态的变化。(等子进程退出,回收子进程)
头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_t wait(int *status);
返回值:
成功:返回被等待进程pid,失败:返回-1
参数:
输出型参数,获取子进程退出状态(信号+退出码),若不关心子进程退出状态设置成为NULL
写一个测试程序:
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
exit(0);
}
//父进程
sleep(10);
pid_t ret_id = wait(NULL);
printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d\n", getpid(), getppid(), ret_id);
sleep(5);
}
发现在子进程退出后,子进程进入僵尸状态(Z状态),等待父进程wait接收子进程,父进程接收后,子进程完全退出,父进程过一会也退出。
父子进程是同时推进的,那么父进程在wait等待子进程退出的这段时间里在干什么呢?
——父进程在这段时间内一直在等待子进程,子进程不退,父进程也不退。
9.waitpid
头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_ t waitpid(pid_t pid, int *status, int options);
参数:
pid:>0表示等待指定id的进程;==-1表示等待任意一个子进程(与wait等效)
status:输出型参数,获取子进程退出状态(信号+退出码),若不关心退出状态可以直接传NULL
返回值:
成功:返回被等待进程pid,失败:返回-1,还在等待:返回0
参数status(位图)
子进程的退出状态有两种:
退出码(代码正常执行结束) + 信号(代码运行异常)
- 代码正常执行结束:退出码,运行正确/不正确。
- 代码运行异常:信号
那么函数的第二个参数status,要如何存储两个数据(信号与退出码)呢?
——不要讲status看成一个完整的整数,而将其看做位图:
所以可以按如下程序来测试获取进程的退出码和信号:
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
exit(100);
}
//父进程
int status = 0;
pid_t ret_id = waitpid(id, &status, 0);
printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
}
结果:
发现获取的退出码等于100,与程序中子进程exit的一样,并且退出信号等于0,表示代码正常运行结束,但是结果不正确。(若将子进程exit改为0,则表示代码正常运行结束并且结果正确)
假如程序出现异常,则信号不等于0,具体退出信号可以在文章后面看。
解析:
(status>>8)&0xFF——获取status的次低8位;
status&0x7F——获取status的低7位;
从status中获取退出码的方法
退出码的获取:
- 可以通过上面的方式获取:(status>>8)&0xFF
- 可以通过宏获取:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
参数options
父进程在wait的时候,如果子进程没有退出,父进程在干什么?
——子进程没有退出的时候,父进程只能一直调用waitpid进行等待,这种等待称之为阻塞等待。
如何理解阻塞等待?
——父进程在阻塞等待时肯定不是运行状态,那么就不在运行队列,那么此时父进程就在阻塞队列中(状态由R->S)。等子进程退出,再重新将父进程放回运行队列中去(状态变回R状态)。
如何让父进程在等待子进程期间不阻塞等待,去做其他的事呢?
——使用非阻塞等待,waitpid默认的方式是阻塞等待。
通过修改第三个参数可以改变等待的策略:
options的参数:
- 0:阻塞等待
- WNOHANG:非阻塞等待
下面写一段程序来测试一下非阻塞等待的效果:
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
exit(100);
}
//父进程
while(1)
{
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0)
{
printf("waitpid error!\n");
exit(1);
}
else if(ret_id == 0)
{
printf("子进程还未退出,父进程先去干别的\n");
sleep(1);//表示父进程去干别的了
continue;
}
else
{
printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
break;
}
}
}
运行结果:
理解:
非阻塞等待就是父进程每过一段时间定期询问一次子进程是否退出,如果退出了那就接收,如果没退出那么就去执行别的程序。
而阻塞等待就是父进程直接停下来等待子进程退出,不会自己去执行别的程序。
简单非阻塞等待的程序学习例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define TASK_NUM 10//任务个数
//模拟预设一批任务
void sync_disk(){
printf("这是刷新数据的任务!\n");
}
void sync_log(){
printf("这是同步数据的任务!\n");
}
void net_send(){
printf("这是网络发送的任务!\n");
}
//函数指针
typedef void (*func_t)();//typedef void (*func_t)(); == typedef void (*)() func_t;
//任务列表
func_t other_task[TASK_NUM] = {NULL};
//载入任务
int LoadTask(func_t func)
{
int i = 0;
for(i; i < TASK_NUM; i++)
{
if(other_task[i] == NULL)
{
break;
}
}
if(i == TASK_NUM)//任务加载满了
{
return -1;
}
else//可以加载任务
{
other_task[i] = func;
}
return 0;
}
//初始化任务
void InitTask()
{
int i;
for(i = 0; i < TASK_NUM; i++)
{
other_task[i] = NULL;
}
//载入三个任务
LoadTask(sync_disk);
LoadTask(sync_log);
LoadTask(net_send);
}
//运行任务
void RunTask()
{
int i;
for(i = 0; i < TASK_NUM; i++)
{
if(other_task[i] == NULL)
{
continue;
}
other_task[i]();
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("这是子进程,子进程还没退出,还有%dS退出,pid:%d,ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
exit(100);
}
//父进程
InitTask();
while(1)
{
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0)
{
printf("waitpid error!\n");
exit(1);
}
else if(ret_id == 0)
{
//子进程还未退出,父进程先去干别的
RunTask();
sleep(1);
continue;
}
else
{
if(WIFEXITED(status))
{
printf("wait success, child exit code : %d\n", WEXITSTATUS(status));
}
else
{
printf("wait success, chile exit signal : %d\n", status&0x7F);
}
//printf("这是父进程,等待子进程结束,pid:%d,ppid:%d, ret_id %d, child exit code:%d, child exit signal:%d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status&0x7F);
break;
}
}
}
10.系统提供的信号
查看系统提供的信号:kill -l
有如下信号:1~31为普通信号
当程序中有错误自己崩溃的时候会根据不同的错误返回不同的信号(野指针…);
如果用kill -9来手动杀一个进程,则进程的退出信号为9;
父进程是如何获取子进程的退出信息的?
——在描述进程的内核数据结构task_struct(pcb)中存在两个整数:int exit_code;
和int exit_signal;
当子进程在执行完毕的时候,会将main函数的返回值写到exit_code中;
如果出现了异常,OS会把进程异常时遇到的信号的编号写到exit_signal中;
OS会将pcb维护起来(因为在OS内核当中),我们要用waitpid这样的系统调用接口,它们会在进程列表当中找到要等待的目标进程(根据pcb中存的子进程pid找到),然后按照前面说的位图格式,把exit_code和exit_signal设置进入waitpid的第二个参数status中,所以用户就取到了子进程的这两个特殊信息。
学习的命令
获取退出码:
echo $?
注:只会保留最近一次执行进程的退出码。