头文件:#include <unistd.h>
函数原型:pid_t fork(void);
作用:从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:给子进程中返回0,父进程返回子进程的pid,创建失败返回-1。
当一个进程调用fork之后,就有代码完全相同的进程。而且它们都运行到相同的地方。通过判断fork的返回值并配合if语句可以让父子进程分流,执行各自的代码,看如下程序:
编译运行:
$ ./myproc
child pid is 1645, fork return 0
father pid is 1000, fork return 1645
fork函数是存在于内核空间的,进程调用fork,控制转移到内核中的fork代码,内核完成如下任务:
为何给子进程返回0,给父进程返回子进程的pid?
在现实生活中,父亲:孩子 = 1:n,即一个父亲可以有多个孩子,但一个孩子只能有一个父亲。父亲的多个孩子在一起时,父亲会具体叫某个孩子的名字,这样这个孩子才会知道父亲在叫自己;但所有孩子都只会叫他们的父亲爸爸。
进程也一样,一个父进程有多个子进程,每个子进程要执行父进程交给它们的任务,想要知道子进程执行的怎么样,父进程必须明确区分每个子进程,所以必须得到它们的pid,即子进程必须要被父进程特殊标识,而父进程不需要被子进程特殊标识。
子进程从哪里开始运行?
fork之后,父进程继续往后运行,而子进程也是从fork之后的位置开始运行,谁先运行有调度器决定。当然子进程也跟父进程代码是共用同一份的,只是子进程不执行fork之前的代码罢了。
什么是写实拷贝?
通常,父子代码共享,当二者都不对代码里的共用数据写入时,数据也是共享的,当任意一方试图写入,操作系统会另外给要写入的数据再开辟一块空间,并更新页表的映射关系。
fork使用场景
fork调用失败的原因
PS:通过进程的返回值(也叫作退出码)判断运行结果是否正确,一般规定返回0表示正确,非0表示错误。
我们可以通过命令:echo $? 来查看最近一次进程运行结果的退出码。
进程收到某个信号,而该信号使程序终止。比如下面程序,我们有进行野指针的访问,编译器检查到后会报告给操作系统,之后系统发送段错误信号并终止进程:
PS:进程如果是异常退出,那么它的退出码是没有任何意义的。
下面我们讨论进程正常退出时的其中两种方式,即exit和_exit,他们是两个不同的函数。
_exit函数
头文件:#include <unistd.h>
原型:void _exit(int exit_code);
作用:直接终止整个进程。
参数:进程的退出码。
exit函数
头文件:#include <unistd.h>
原型:void exit(int status);
作用:先执行用户通过 atexit或on_exit定义的清理函数,然后刷新缓冲区,最后调用_exit来终止整个进程。
参数:进程的退出码。
exit()函数的底层最终还是会调用_exit()来终止整个进程,不过在这之前会完成一些该进程相关清理工作。
通过一段代码感受一下,_exit因为没有刷新缓冲区,所以什么都没输出。
子进程想要完全退出,最后必须由父进程调用wait、waitpid函数来等待子进程退出,回收资源和获取子进程退出状态。
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而出现内存泄漏,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。而且,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
总结进程等待的作用有两个:
有两个函数可以完成进程等待:wait和waitpid,它们两个都属于系统调用函数。
wait
头文件:#include<sys/wait.h> 和 #include<sys/wait.h>
原型:pid_t wait(int* status)
参数:输出型参数,获取任意一个子进程退出状态,不关心则可以设置成为NULL。
返回值:等待成功(包括子进程正常和异常退出)返回被等待进程pid、子进程还没结束就继续等、等待失败返回-1(进程不存在)。
waitpid
头文件:#include<sys/types.h> 和 #include<sys/wait.h>
原型:pid_ t waitpid(pid_t pid, int *status, int options)
参数:
返回值:
总结:waitpid就是wait的升级版,它相比于wait而言可以指定要等待那个子进程(wait是等待任意一个子进程)和可以实现非阻塞式等待(wait只能阻塞式等待)。
wait和waitpid,都有一个status参数,即子进程的退出状态码,该参数是一个输出型参数,由操作系统赋值。如果传递NULL,表示不关心子进程的退出信息,否则操作系统会把这些子进程的退出信息(包括信号和退出码)通过status这个输出型参数反馈给父进程。
status不能简单的当作整形,而应当作位图来看待,我们只研究status低16比特位。
解析状态码的方法有两种:位运算和宏。
方法一:通过位运算解析状态码
样例:
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid==0)//子进程
{
// 子进程等待30秒后才退出
sleep(30)
exit(2);// 子进程退出码为2
}
else if(pid>0)//父进程
{
// 父进程里定义的输出型参数,传入wait,用来获取子进程的退出状态
int st=0;
int ret=wait(&st);
if(ret>0 && (st&0x7f)==0)//等待成功且子进程正常退出
{
printf("child exit code is:%d\n",(st>>8)&0xff);
}
else if(ret>0 && (st & 0x7f)>0)//等待成功且子进程异常退出
{
printf("child sig code is:%d\n",st&0x7f);
}
}
return 0;
}
正常情况等待30秒后输出:
child exit code is:2
如果在等待30s期间,在另外一个终端通过kill -9 杀死子进程,会出现:
child sig code is:9
方法二:通过宏解析状态码
1、WIFEXITED(status) 即 "wait if exited"缩写,若此值为真,表明进程正常结束。此时可通过 WEXITSTATUS(status) 即 "wait exit status"缩写,来获取进程退出码。
// 其中status为输出型参数,就是子进程的退出状态
if(WIFEXITED(status))
{
printf("退出码为:%d\n", WEXITSTATUS(status));
}
2、WIFSIGNALED(status) 即"wait signaled"缩写,非0表明进程异常终止。此时可通过 WTERMSIG(status) 即"wait term signal"缩写,获取进程的退出信号。
// 其中status为输出型参数,就是子进程的退出状态
if(WIFSIGNALED(status))
{
printf("退出信号为:%d\n", WTERMSIG(status));
}
样例:
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid==0)//子进程
{
sleep(30);
exit(2);
}
else if(pid>0)//父进程
{
int st=0;
int ret=wait(&st);
if(ret>0 && WIFEXITED(st))// 等待成功且子进程正常退出
{
printf("child exit code is:%d\n",WEXITSTATUS(st));
}
else if(ret>0 && WIFSIGNALED(st))// 等待成功且子进程异常退出
{
printf("child sig code is:%d\n",WTERMSIG(st));
}
}
return 0;
}
等待30秒
child exit code is:2
如果在等待30秒期间,在另外一个终端kill -9 杀死子进程,会出现
child sig code is:9
运行到wait或waitpid时,如果子进程还没退出,父进程就停在这里不动直至子进程退出,这就叫做阻塞式等待。其中wait只能阻塞式等待,而waitpid的第三个参数option传0时才是阻塞式等待。
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid==0)//子进程
{
sleep(30);
exit(2);
}
else if(pid>0)//父进程
{
int st=0;
int ret=waitpid(-1,&st,0);//阻塞式等待
if(ret>0 && WIFEXITED(st))
{
printf("child exit code is:%d\n",WEXITSTATUS(st));
}
else
{
printf("wait child fail\n");
}
cout<<"I'm here<<endl;
}
return 0;
}
等待30秒
child exit code is:2
I’m here
父进程运行到waitpid时,若子进程还在运行中就继续做父进程自己的事情,完成后再来检测子进程是否退出了,一直重复这个过程就是非阻塞式等待。waitpid的options传WNOHANG即"wait no hang",如果检测到子进程还未退出,返回0。
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid==0)//子进程
{
sleep(3);
exit(2);
}
else if(pid>0)//父进程
{
int st=0;
int ret=0;
do
{
ret=waitpid(-1, &st, WNOHANG);//非阻塞式等待
// 检测到子进程还没有退出,父进程先做自己的事
// 做完成后,再来检测子进程是否退出
if(ret==0)
{
sleep(1);
printf("haha\n");
}
}while(ret==0);
// 等待完成后的处理
if(ret>0 && WIFEXITED(st))
{
printf("child exit code is:%d\n",WEXITSTATUS(st));
}
else
{
printf("wait child fail\n");
}
cout<<"I'm here<<endl;
}
return 0;
}
编译运行:
haha
haha
haha
child exit code is:2
I’m here
进程替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间)。
子进程可以执行与父进程不同的程序,这样子进程的执行就会更加独立、灵活。
其中有六种以exec开头的函数,这一系列统称exec函数。
头文件:#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 execvpe(const char *file, char *const argv[],char *const envp[]);
返回值
参数理解
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
1、带p与不带p
带p的话第一个参数就不用写明可执行程序的路径(绝对路径或相对路径都可以),只需写出命令的名字就行,它会像系统执行命令一样到PATH环境变量里它所指定的各目录中搜寻该可执行文件。
我们有两个同一目录下编译后的可执行程序:myproc和myexec,他们的代码如下
我们运行myproc程序,里面又通过 execl() 函数把当前程序替换为另一个程序myexec。
$ ./myproc
**** before exec ****
agrv[0] = myexec
agrv[1] = hello world
可以看到,经过 execl() 函数替换后,原程序最后的“after exec”不再执行,而是去执行另外一个程序myexec去了,即替换成功后不再返回。
接下来我们使用带有p的 execlp(),这样我们第一个参数只需写我们想要执行的程序的名字就可以了。
$ ./myproc
**** before exec ****
**** after exec ****
看结果,我们并没有替换成功,因为环境变量PATH里的路径中没有myexec这个程序,我们把myexec拷贝到PATH下的其中一个路径/bin后在试试看:
$ sudo cp ./myexec /bin
$ ./myproc
**** before exec ****
agrv[0] = myexec
agrv[1] = hello world
这次成功了,所以对于带p的函数,必须先保证我们想要替换的程序必须能在环境变量PATH里找到才行。
2、 带l和带v
带l(即list)的函数:你需要把命令和选项作为参数依次、逐个的传入,最后要用空指针标识结束。
execl("myexec","myexec","hello world",NULL);
带v(即vector)的函数:要求把命令和选项同时放到一个数组里,最后要用空指针标识结束。调用时只需把数组传入即可。
char* const arr[]={"myexec","hello world",NULL};
execv("./myexec",arr);
3. 带e的函数
包括 execle()、execvpe(),替换前可以传递一个指向环境字符串的指针数组。
参数例如char* myenv[ ] = {“AA=111”,“BB=222”,“CC=333”,NULL},带e的话就表示该函数读取myenv[ ]数组,而不使用默认的系统配置的环境变量。即使用传入的环境变量,替换了默认的环境变量。
以 execle() 为例,我们重新编写同一目录下的myproc.c和myexec.c两个文件
生成可执行程序后,编译运行
$ ./myproc
**** before exec ****
AA=111
BB=222
CC=333
可以看到替换后的程序myexec确实使用了我们替换前传入的自己写的环境变量myenv。
什么是Shell
hell是指提供使用者使用界面的软件,它接收命令,然后调用相关的应用程序。
Shell实现原理
shell作为父进程用fork建立子进程,用exec系列函数簇在子进程中运行用户指定的程序,父进程shell用wait命令等待其子进程结束。wait系统调用同时从内核取得退出状态或者信号序列以告知子进程是如何结束的。
代码实现
include <iostream>
#include
#include
#include
#include
#include
using namespace std;
int main()
{
while(1)
{
// 获取命令行 + 解析命令行
cout<<"[myshell]$ ";
char* argList[20] = {nullptr};
string s;
string tmp;
int i = 0;
vector<string> v(20);
// 1、获取一行命令行,存储在string类型对象s中
getline(cin, s);
// 2、遍历s,取出其中的每一个命令和选项,先放到数组v中,完成字符串内容的深拷贝
// 在把每一个元素的指针存到指针数组argList里
for(auto e : s)
{
if(e == ' ')
{
v[i] = tmp;
argList[i] = (char*)v[i].c_str();
++i;
tmp.clear();
}
else
{
tmp += e;
}
}
// 最后一个命令还没有存放,因为我们输入的一行字符串最后一个字符不是以空格结尾的
argList[i] = (char*)tmp.c_str();
// 3、父子进程分流完成各自的任务
// 子进程:用execvp完成程序替换
// 父进程:等待子进程
pid_t id = fork();
if(id == 0)// 子进程
{
execvp(argList[0], argList);
exit(-1);
}
else if(id > 0)// 父进程
{
int status = 0;
int ret = wait(&status);
if(ret > 0)// a、等待成功
{
if(WIFEXITED(status))// 正常退出
{
cout<<"child exit code is:"<<WEXITSTATUS(status)<<endl;
}
else// 异常退出
{
cout<<"abnormal exited"<<endl;
}
}
else// b、等待失败
{
cout<<"wait fail"<<endl;
}
}
}
return 0;
}