在linux中fork函数是非常重要的函数,它的作用是从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
其使用方法为:
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,创建失败给父进程返回-1。
当子进程被创建出来,代表系统中多了一个进程,那么OS就需要对这个新增的进程进行管理,就需要一份与进程相关的内核数据结构(PCB),PCB由OS创建(以父进程的PCB为模板),而子进程的代码和数据则是继承于他的父进程,代码不会改变,继承也不会有问题,但是如果数据也被继承了,那么当父子进程要对数据进行修改操作时,如何保证进程之间的独立性?
OS会以父进程的内核数据结构(PCB)为模板创建子进程,那么子进程自然也就会继承父进程的数据段和代码段。所谓继承就是他们的数据段和代码段的内容在物理内存上是同一块空间,父进程映射的位置也是子进程映射的位置。
当OS发现了进程要对只有只读权限的数据进行修改时,会让子进程先中断,然后重新映射,映射好了之后再让子进程重新运行。
这就是fork创建子进程写时拷贝的过程。
1.根据fork给父子进程的返回值不同,让子进程和父进程执行不同的代码。
2.让进程执行不同的程序,比如子进程从fork返回后可以调用进程程序替换函数。
1.系统中的进程太多
2.实际用户的进程数超过了限制
进程会退出一般是以下的三种情况:
1.代码运行完毕,并且结果正确
2.代码运行完毕,但是结果错误
3.代码没有运行完,中途异常终止了
当我们遇到第三种情况,进程异常终止了,那我们如果想知道终止的原因,其实是有很多种可能性的,如何确定是什么原因导致的?解决方案就是用一个数字来代表一种可能,那么OS只要知道进程结束后返回的数字是几,那就自然知道这个进程运行的情况了,而这个用来衡量进程的运行情况的数字就是进程的退出码。
我们写C语言的main函数总是要在最后return 0,这里return的0就是进程的退出码,0表示进程的运行一切正常。
想要查看进程的退出码,有以下两种方法:
1.可以通过命令:echo $?
打印命令行中上一个退出的进程的退出码
2.可以用strerror函数查看不同退出码代表的情况,使用方法如下
表示打印从0到149的退出码代表的情况,退出码的范围大小是0-255,但是后买的内容不一定有代表的情况,所以我只打印到149,下面是打印的内容
进程退出的方式大体上可以分为两种:
1.异常退出
即进程收到了某种信号让进程退出,比如我们用ctrl+c结束进程
当写一个如下的代码
#include <stdio.h>
int main()
{
while(1)
{
printf("I am %d\n",getpid());
sleep(1);
}
return 0;
}
当我们用ctrl+c结束进程时,退出码为130
当我们用kill -9结束进程时,退出码为137
这些退出码都没有意义。
2.正常终止(这种终止可以用echo $?查看退出码)
正常终止分三种:
(1)main函数的return,代表进程退出(非main函数的return代表的是返回)
当我把return值设置成5,在外面查看到的退出码就是5。
(2)调用exit函数
使用方法如下:
#include <stdlib.h>
void exit(int status);
这里的status就是退出码,
这里的退出码是exit设置的5而不是return设置的0,说明进程是因为exit退出的,并且这个exit并不在main函数内,但是他仍然能让进程退出,也就是说,exit在任何地方调用都可以终止该进程。
(3)调用_exit函数
使用方法如下:
#include <unistd.h>
void _exit(int status);
_exit函数的使用方法和exit的基本一样,他们的区别在于_exit不会进行后续的收尾工作,比如刷新用户缓冲区,不会关闭流,不会执行用户定义的清理函数。
printf函数在打印内容时打印的内容不是直接打印到屏幕的,而是先放到用户级的缓冲区,然后等缓冲区刷新的时候再打印出来,‘\n’就可以让缓冲区刷新,而上面的代码没有’\n’,所以按道理不会刷新,运行以后也一直没有东西打印出来,但是3秒过后Hello Linux就会被打印出来,原因就是exit和return退出进程的时候会要求系统进行缓冲区刷新,所以我们才看到了我们要打印的内容。
用_exit就不会刷新缓冲区
什么是进程等待:
在系统层面上,进程退出就代表少了一个进程,那么该进程对应的PCB,虚拟地址空间,页表和各种映射关系,代码和数据,申请的空间都要被释放掉。
我们创建子进程的目的可能是让子进程去完成某种任务,而父进程需要知道子进程的情况,让父进程在fork之后通过wait/waitpid等待子进程退出,然后释放掉子进程的资源并且获取到子进程的运行信息。
为什么需要进程等待
1.通过获取子进程的退出信息(不止退出码),能够得知子进程的执行结果
2.保证时序问题,子进程先退出,父进程后退出(防止出现孤儿进程)
3.进程退出时会先进入僵尸状态,通过父进程wait回收掉子进程的占用的资源(僵尸资源),可以避免造成内存泄漏的问题
进程一旦进入僵尸状态,用kill -9也无法杀掉一个已经死掉的进程,只能通过杀掉父进程的方式让他变成孤儿进程然后系统会自动回收他的资源
其使用方法如下
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status)
该函数是由父进程调用,等待成功返回子进程的PID,等待失败返回-1,参数status是一个输出型参数,可以用来获取子进程的退出状态(退出码),如果不关心可以设置为NULL
使用方法如下
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int* status,int options);
返回值:返回等待到子进程的PID
pid:表示等待一个指定PID的子进程,如果想等待任意一个子进程可以用-1
status:拿到子进程的退出状态
option:等待方式,0表示阻塞等待,WNOHANG表示非阻塞等待
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
int main()
{
pid_t id=fork();
if(id==0)//子进程
{
for(int i=0;i<5;i++)
{
printf("I am:%d,my father is:%d\n",getpid(),getppid());
sleep(2);
}
exit(5);
}
else //父进程
{
int status=0;
pid_t ID=waitpid(id,&status,0);
if(ID>0)
{
printf("I am father:%d,wait:%dsuccess!,status:%d\n",getpid(),ID,status);
}
else
printf("wait error!\n");
printf("father is running!\n");
sleep(1);
}
return 0;
}
上面的代码的作用是创建一个子进程,子进程每隔两秒打印一次,打印五次之后exit退出,父进程通过waitpid对子进程进行等待,通过运行结果发现等待是成功的,但是这个status并不是我在exit上设置的5,这是为什么?
在上面我们说过进程的退出情况大体上分为两种,分别是正常退出和异常退出,正常退出会有一个退出码,可以查看退出码了解到进程为什么退出,但是异常退出的退出码我们认为没有意义,那我们怎么知道进程是异常退出还是正常退出?这个参数status有关。
进程异常终止的本质是这个进程因为异常问题,导致自己收到了某种信号
status的类型是一个int,但是我们不能把它当作简单的整形看待,可以把它当作位图看待,假设我们是32位的机器,那status就有32个bit位,高16个bit位暂时不关心,其低16个bit位的结构如下:
其高8位代表的是进程的退出码,第8个bit位代表core dump标志,低7位代表的是终止信号,如果进程是正常退出,那就是高八位的退出码起作用,如果进程是异常终止,那他就会收到信号而结束,低七位就是其收到的信号。
可以用位操作来查看一下进程的退出码和终止信号,根据status的结构
退出码:(status>>8)&0xFF
终止信号:status&0x7F
我们在上面的查看status的代码中再加上查看退出码与退出信号的部分
printf("I am father:%d,wait:%dsuccess!,status:%d\n,退出码:%d,终止信号:%d",getpid(),ID,status,(status>>8)&0xFF,status&0x7F);
结果如下:
除了使用位操作在status里拿到退出码与终止信号,还可以用宏
WIFEXITED(status):如果是正常终止的进程就返回真
WEXITSTATUS(status):如果正常退出,就返回进程的退出码
这次使用宏来验证一下,把父进程的代码稍作修改:
int status;
pid_t ID=waitpid(id,&status,0);
if(ID>0)
{
if(WIFEXITED(status))
{
printf("wait success!退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait failed!\n");
}
}
等待就是父进程等待子进程退出。
而等待有两种方式:阻塞等待与非阻塞等待(waitpid等待时的第三个参数option就是用来选择等待方式的)
父进程什么都不干,一直等待子进程,这叫做阻塞等待。
阻塞并不意味着父进程就不会被调度执行了,在kernel(内核)中,当父进程运行到waitpid时,如果进程是阻塞式的等待,那么这时候OS会把父进程的状态从R状态调换成S状态,当子进程退出,父进程再重新回到R状态继续运行。
也就是说,阻塞的本质其实是进程的PCB被放入了等待队列,并且将进程的状态改为了S状态(不再为其分配CPU资源)。而返回的本质就是进程的PCB从等待队列被拿到运行队列,从而被CPU调度
父进程在等待的时候可以做自己的事,等子进程退出时再检测子进程运行状态,这就是非阻塞等待
如果是非阻塞等待,父进程就可以一直运行,但是不能光自己运行,还要想不时地对子进程进行检测,就有了基于非阻塞等待的轮询方案
父进程等待子进程会有两种情况:
1.子进程还没有结束,根本没有退出
(这时候父进程可以继续做自己的事)
2.子进程退出了,可以用wait/waitpid等待
下面是其等待代码:
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t id=fork();
if(id==0)//子进程
{
for(int i=0;i<3;i++)
{
printf("I am:%d,my father:%d\n",getpid(),getppid());//子进程每隔两秒打印一次,一共打印三次
sleep(2);
}
exit(3);
}
else if(id>0)//父进程
{
int status=0;
while(1)
{
pid_t ret=waitpid(id,&status,WNOHANG);//非阻塞等待子进程
if(ret==0)//子进程虽然没有退出,但是waitpid是等待成功的,父进程还需要等待
printf("Do father's things!\n");
else if(ret>0)//等待成功,子进程退出了
{
printf("father wait:%d success,退出码:%d,退出信号:%d\n",ret,(status>>8)&0xFF,status&0x7F);
break;
}
else//waitpid失败
{
perror("waitpid");
break;
}
sleep(1);//每隔一秒检测一次
}
}
else//子进程创建失败
printf("fork fail!\n");
return 0;
}
运行结果如下
fork创建的子进程执行的是还是父进程的代码,如果想让子进程完全执行别的代码(全新的程序),就要用到程序替换
程序的本质是一个文件,文件=程序代码+程序数据。这个文件不运行时是存放在磁盘中的,当运行一个程序时,程序最终会变成一个进程,OS会创建专门的数据结构管理他,有虚拟内存,页表,然后在把数据,代码映射到物理内存。
程序替换就是指让进程不变,只替换进程的代码和数据的技术,用老的进程外壳执行新的进程代码和数据,没有创建新进程。
程序替换的本质就是把程序的进程代码+数据,加载到特定进程的上下文中。
我们写的C/C++程序要运行,就必须要被加载到内存中,加载是通过加载器实现的,而加载器的底层就是封装好的exec*系列的程序替换函数。
并且我们替换子进程的代码并不会影响到父进程,虽然父子进程的代码是共享的,但是和数据一样,不修改时是这样,当我们要修改子进程的代码时,会和修改数据一样发生写时拷贝,子进程会再映射一段代码然后被替换,以此做到父子进程不会相互影响。
程序替换是通过exec系列的函数实现的,只要进程程序替换成功,子进程就不会执行exec函数后面的代码,也不需要对exec*函数进行返回值检测,只要他返回了,那一定是因为调用失败了。
exec* 系列函数的头文件为**<unistd.h>**
int execl(const char* path,const char* args,...);
path:你要执行程序的全路径(所在路径+文件名)
args:后面的内容是一个可变参数列表,在命令行要目标程序怎么执行,参数就怎么传。注意:这里必须以NULL作为参数传递的结束。
路径与运行参数都是以字符串的形式传递!
示例:
int main()
{
pid_t id=fork();
if(id==0)//子进程
{
printf("I am child!\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("execl fail!\n");
exit(1);
}
else //父进程
{
int status=0;
pid_t ret=waitpid(-1,&status,0);
if(ret>0)
printf("wait:%d success!退出码:%d\n",ret,(status>>8)&0xFF);
}
return 0;
}
子进程的代码被成功替换成了ls命令,并且子进程的退出码是0,而不是我们设置的1,表明他并没有执行execl后面的代码。
int execv(const char* path,char* const argv[]);
其与execl完全类似,只是execl的运行参数是可变参数,execv是数组
使用方法:
char* argv[]={"ls","-a","-l",NULL};
execv("/usr/bin/ls",argv);
int execlp(const cahr* file,char* const arg, ...);
不要需要程序的地址,只需要程序的名字,会自动在环境变量PATH中的地址中搜索程序
使用方法:
execlp("ls","ls","-a","-l",NULL);
注意:这里面第一个ls是程序名,用来在搜索程序的,第二个ls是执行程序的方法。
int execvp(const char* file,const char* argv[]);
和execlp一样,可以不用地址自动寻找程序,但是使用的是数组传参
使用方法:
char* argv[]={"ls","-a","-l",NULL};
execvp("ls",argv);
int execle(const char* path,const char* arg, ... ,char* const envp[]);
传入的参数和execl一样,只是多了一个envp[],可以把被替换进程的环境变量替换成自定义的。
在下面的用例我们写两个程序,分别为child和father。
child的功能是打印环境变量,代码如下
father的功能是创建子进程,然后把子进程替换成child,并且给子进程传入一个自定义的环境变量。代码如下:
为了能让Makefile可以同时生成两个可执行程序,可以先定义生成一个all可执行,让all依赖我们需要生成的两个可执行程序,但是不写all的依赖方法,所以最后并不会生成all,但是all依赖的程序会被生成,这样就实现了Makefile一次生成多个可执行程序。写法如下
完成后,先单独运行child,现象如下:
打印出了所有的环境变量,再运行一次father,现象如下:
最开始打印出了"I am child",说明程序替换是成功的,运行的的确是child的代码,但是打印的内容不再是环境变量,而是我们在father的子进程里面自定义的环境变量,这就是把环境变量替换成了自定义的环境变量。
int execve(const char* path,char* const argv[],char* const envp[]);
与execle类似,可以给被替换进程传入一个自定义的环境变量,只是运行参数是用数组
以上面的例子为例,其使用方法为:
char* argv[]={"child",NULL};
execve("./child",argv,env);
上面的六个函数名字非常相似,容易混淆,可以通过exec后面的字母了解他们的功能
l(list):表示用列表传参
v(vector):表示用数组传参
p(PATH):表示可以在环境变量PATH里找程序,第一个参数只需要用程序名,不需要程序路径
e(env):表示可以给被替换程序传入自定义的环境变量
这6个接口其实没有本质上的区别,只是需要的参数不同,以满足不同的应用场景。
函数名 | 传参形式 | 是否需要带路径 | 是否需要自定义环境变量 |
---|---|---|---|
execl | 列表 | 需要 | 不需要 |
execlp | 列表 | 不需要 | 不需要 |
execle | 列表 | 需要 | 需要 |
execv | 数组 | 需要 | 不需要 |
execvp | 数组 | 不需要 | 不需要 |
execve | 数组 | 需要 | 需要 |
这6个接口中只有execve是真正的系统调用,其他5个都是execve封装的。
execve可以在2号手册查到,其他的都是在3号手册查到
通过上面的程序替换,我们可以自己写一个简单的shell程序,其功能就是根据用户输入的内容,执行相应的命令。
实现思路:
(1)让程序不断的打印提示符,等待用户输入命令
(2)获取命令字符串
(3)对命令字符串进行解析
(4)检测用户输入的命令是否需要shell本身执行,即是否为内建命令
(5)如果不是内建命令,就执行第三方命令(通过创建子进程,然后替换子进程实现)
下面是实现代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#define NUM 128
#define CMD_NUM 64
int main()
{
//一定是一个死循环程序,不断地获得字符串,打印提示符
char command[NUM];
for(;;)
{
char *argv[CMD_NUM]={NULL};
//1.打印提示符
command[0]=0;//用这种方式,可以做到以O(1)的时间复杂度清空字符串
printf("[chen@myhostname mydir]# ");
//2.获取命令字符串
fgets(command,NUM,stdin);
command[strlen(command)-1] = '\0';//"ls -a -l\n\0"
printf("echo: %s\n",command);
fflush(stdout);
//sleep(1);
//"ls -a -l"
//将字符串进行分割
//3.解析命令字符串char* argv[];
//strtok();
const char *sep=" ";
argv[0]=strtok(command,sep);
int i=1;
while(argv[i]=strtok(NULL,sep))
{
++i;
}
for(i=0;argv[i];++i)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
//4.检测命令是否需要shell本身执行,即内建命令
if(strcmp(argv[0],"cd")==0)
{
if(argv[1]!=NULL)
chdir(argv[1]);
continue;
}
//5.执行第三方命令
//执行命令,但是不能在当前进程替换,只有一个进程,替换了上面的代码也会被替换
//所以需要用到子进程
if(fork()==0)
{
//child
//用vp,命令在环境变量中可以找到,
execvp(argv[0],argv);
exit(1);
}
waitpid(-1,NULL,0);
}
return 0;
}
以上就是本片的全部内容。