目录
秃头侠们好呀,今天来说 进程控制
进程创建
fork函数的认识
fork函数的作用是在已存在的进程中创建一个新进程,新进程为子进程,原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构(包括:进程控制块PCB、进程地址空间、页表、构件映射关系)内容拷贝至子进程
3、添加子进程到系统进程列表当中
4、fork返回,开始调度器调度
写时拷贝
通常父子代码共享,父子在不写入时,数据也是共享的,但当任意一方需要进行写入,就会以写时拷贝的方式各自拥有一份副本数据。
当有人要写入的时候,发生缺页中断,OS把数据拷贝一份,再让映射关系修改一下,使其具有独立性,父子进程互不干扰。(详细看下图)
fork常规用法
①一个父进程希望复制自己,使父子进程同时执行不同的代码段。比如:父进程等待接待客户请求,子进程处理请求。
②想让子进程执行完全不同的一个程序。比如,子进程从fork返回后,调用exec进程替换。
其实就是要么子进程做父进程的一部分工作,要么子进程去做全新的工作。
(子承父业/自己创业)
fork失败的原因
创建进程是有成本的(时间、空间),如果系统中有太多进程,或者实际用户的进程数过多,都有可能造成fork失败。
进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果错误
- 代码异常终止(没跑完就终止了),程序崩溃(退出码没意义)
进程常见退出
正常终止(可以通过 echo $? 查看进程最近一次的退出码)。
- 从main返回
- 调用exit
- _exit
我们熟知的main()函数,最后我们都要return 0是为啥?
答:0是进程的退出码,表示成功退出。而!0则是不正确退出,具体为什么不正确,有多种可能,用具体一个数代表一种可能性。
strerror(i) 打印错误码对应的原因。
①main()函数return 代表进程退出,非main()函数,return叫函数返回
②exit(i)在任意地方调用,终止进程,参数为退出码
③_exit(i)强制终止进程,不要给我进行后续收尾工作了(比如刷新用户级缓冲区)
而①②本身会要求系统进行缓冲区刷新。
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返
回值当做 exit的参数。
进程退出时,OS层面做了什么呢?
:系统层面少了一个进程,free PCB、free mm_struck(进程地址空间)、free 页表和映射关系,代码和数据申请的空间也要给释放掉。
进程等待
进程wait是什么?
:通俗说,fork():子进程帮助父进程完成任务,那父进程得知道你完成咋样了吧。
为什么要让父进程等待呢?
- 通过获取子进程的退出信息,能够得知子进程执行结果
- 可以保证时序问题,子进程先退出,父进程后退出
- 进程退出后会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放该子进程占用的资源。
- 进程一旦变成僵尸状态,刀枪不入,因为该进程以死,无法给它发送信号(kill -9也无能为力)。
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
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非零,提取子进程退出码。(查看进程的退出码)
if(WIFEXITED(status))
{
printf("exit code:%d\n",WEXITSTATUS(status));
}
else
{
printf("error.get a signal\n");
}
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
父进程拿到什么status结果,一定和子进程如何退出强相关。
子进程如何退出就是刚才我们谈到的那三种状态(1完毕正确、2完毕错误、3异常)。
最终一一定要让父进程通过status获得子进程执行的结果。
代码异常终止的本质:是这个进程因为异常问题,导致自己收到了某种信号(正常为0)。
如果没有收到信号就是跑完了,这时再看退出码,看是正确跑完还是错误跑完。
status不能简单看作整形
(32位比特位,只用低16位)
int main()
{
printf("I am child pid:%d,ppid:%d\n",getpid(),getppid());
exit(10);
}
bash是命令行启动的所有进程的父进程。
bash一定是通过wait方式得到子进程的退出结果,所以我们echo $?能够查到子进程的退出码。
我们画图理解一下waitpid的status是怎么获取子进程信息的
wait和waitpid都可以回收子进程的僵尸!
第三个参数:options
如果设为0则是默认行为:阻塞等待,父进程啥也不干,就等此进程退出。
如果设为WNOHANG,等待方式为非阻塞,需不断检测它的运行状态,但不会卡住自己,还可以做自己的事情。可能需要多次检测,基于非阻塞等待的轮询方案。
举个例子,比如你打电话让朋友下楼给你送东西,但是这时朋友正在忙个事,它让你等几分钟,如果不挂电弧等他,就是第一种,如果你挂了电话玩会游戏,停2分钟再给他打问好了没,就是第二种。
阻塞了是不是意味着父进程不被调度执行了呢?
:
阻塞的本质是:进程的PCB被放入等待队列,并将进程的状态设为S状态。
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度。
WNOHANG:非阻塞
1、子进程根本没退出(父进程可以做自己的事,不用干等)
2、子进程退出,waitpid(调用成功/失败)
int status=0;
while(1)
{
pid_t ret=waitpid(-1,&status,WNOHANG);
if(ret==0)
{
//子进程没退出但是waitpid是成功的,需要父进程重复等待,可以做自己事
}
else if(ret>0)
{
//子进程退出了,waitpid也成功了,获取到了对应的结果
printf("%d,%d,%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
else{
//等待失败
perror("waitpid");
break;
}
}
进程程序替换
目前,我们创建子进程是为了什么?
通过if else if让子进程执行父进程代码的一部分。
那如果想让子进程执行全新的代码呢?
使用:程序替换!
进程不变,仅仅替换当前进程的代码和数据的技术叫做进程的程序替换
它没有创建任何新的进程,所以进程id不变,而是用老进程的壳子。
本质就是:把新的代码数据加载到老壳子里(特定进程的上下文)。
程序本质也是一个文件
文件=程序代码+程序数据(都在磁盘上)
C/C++程序想要运行,必须先加载到内存中。
如何加载?加载器!
而exec*程序替换函数就相当于加载器。
进程是具有独立性的!
子进程进行替换,为何父进程没有受影响?父子进程不是代码共享吗?
:进程替换会更改代码区的代码,就要发生写时拷贝了!
只要进程的程序替换成功,就不会执行后续代码,意味着exec函数成功的时候是没有返回值的,不需要返回检测。
所以只要exec返回了,就一定是调用失败了。返回-1。
基本使用
#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[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
怎么在Makefile一次形成多个执行
.PHONY:all
all:myexe myload
myexe:myexe.c
gcc -o $@ $^
myload:myload.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f myexe myload
所有接口没有太大差别,只有参数不同,为了适应不用应用场景。
execve是系统调用接口,其他都是库函数,就是对系统调用做了不同封装而已。
模拟实现shell命令行解释器
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#define NUM 128
#define CMD_NUM 64
int main()
{
char command[NUM];
while(1)
{
char*argv[CMD_NUM]={NULL};
command[0]=0;
printf("[who@myhostname mydir]# ");
fflush(stdout);
fgets(command,NUM,stdin);
command[strlen(command)-1]='\0';//清理一下\n
const char*sep=" ";
argv[0]=strtok(command,sep);
int i=1;
while(argv[i]=strtok(NULL,sep))
{
i++;
}
if(strcmp(argv[0],"cd")==0)//检测命令是否需要shell本身执行,内建命令
{
if(argv[1]!=NULL)
{
chdir(argv[1]);
}
continue;
}
if(fork()==0)
{
execvp(argv[0],argv);
exit(1);
}
int status=0;
waitpid(-1,&status,0);
printf("exit code:%d\n",(status>>8)&0xFF);
}
return 0;
}
⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流