Linux中的fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include //头文件
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
我们看下面的代码:
int main()
{
pid_t id = fork();
int cnt = 0;
if(id>0)
{
while(1)
{
printf("父进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);
sleep(1);
}
}
else if(id==0)
{
while(1)
{
printf("子进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);
cnt++;
sleep(1);
}
}
else{
printf("fork fail\n");
return -1;
}
return 0;
}
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。fork之前由父进程独立运行,fork之后父子进程分别执行。当然了,fork之后,谁先执行完全由调度器决定。
如何理解fork之后,给父进程返回子进程的ID,给子进程返回0呢?
这是因为父进程可能有多个子进程,但是子进程只能有一个父进程,所以需要给父进程返回子进程的ID,将子进程管理起来。
如何理解if-else-if-else语句能够同时执行呢?
当fork函数在
return
之前就已经将子进程创建好了,创建好的子进程可能已经在运行队列中了,而且fork之后代码是供父进程和子进程共享的,在fork内部已经有父子两个执行流了,因为两个return语句都将会被执行,但是两个执行流谁先执行是由调度器决定的,因为返回的本质是写入,所以谁先返回就将谁写入id,由于写时拷贝的发生,所以地址一样,但是内容不一样,因此 就可以让父子进程执行不同的代码了。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
fork的常规用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因:
- 系统中有太多的进程
- 实际用户的进程数超过了限制
我们创建进程的目的是为了让进程帮我们执行任务,但是任务执行的结果如何应该如何衡量呢?如果一个程序正常运行完了,那他会有两种结果:结果正确
或者结果不正确
,一般来说,不同的退出码表示的是不同的结果。
一般来说:0表示运行结果正确,非0表示运行结果不正确。这里我们来看一下C语言中的库函数strerror
,他表示从内部数组中搜索错误号errnum,然后返回一个指向错误消息的字符串的指针。
查看进程的退出码
在Linux下,我们可以使用echo $?
来查看进程退出码,但在这里我们需要注意的是它只能查看最近的一个进程的进程执行完成时的退出码。
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程退出的方法
进程正常退出有如下几种方法:
- 从main函数返回
- 调用exit
- 调用_exit
库函数:exit
exit
的用法:
exit函数将我们的程序直接终止,并没有执行exit之后的语句,exit后,组成进程的程序,数据,进程控制块全部消失。
系统调用:_exit
下面我们对上面的程序调用,系统调用的_exit函数来看一下效果:
这里我们可以看到:它的效果和exit的效果是一样的。
_exit
和exit的区别
既然exit和_exit的效果一样,那为什么要出现两份呢?其实他们还是有区别的。
这里我们需要知道的是exit
的内部调用了_exit
,为什么调用exit会打印,而调用_exit不会打印呢?这是因为数据将会先被写入缓冲区,待缓冲区刷新的时候才能被写入到显示器上。在上面的程序中,因为没有使用'\n'
进行行缓冲的刷新。所以exit在终止程序后会刷新缓冲区,而_exit不会。但由于exit的底层封装了_exit,所以我们可以得出结论:缓冲区并不在操作系统内部,而是在用户空间。
进程异常退出
我们的进程也是可以异常退出的,比如:Ctrl C终止进程、或者程序中有遇到野指针,/0、空指针野指针等问题。
例如,当我们在程序中使用一个整数去除零的时候:
进程等待指的是通过系统调用获取子进程退出码或者退出信号的方法。
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的本质就是父进程需要通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。
如果等待进程终止成功,wait函数将会返回终止进程的id值,等待失败则会返回-1,下面我们来看一下wait函数。
我们先来看一下wait的用法:
子进程在执行完之后的五秒内由于父进程处于休眠状态,所以子进程正在处于僵尸状态,五秒过后父进程调用wait回收子进程,子进程由僵尸状态退出结束了进程。最后父进程在等待五秒后自动退出。
如果我们将最后一个options设置了WNOHANG,如果调用中waitpid发现没有已退出的子进程可以收集,则返回0;如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误信息。
下面我们改一下我们的代码:
但在这里我们发现status并不是我们想要得到的111,这里的status表示的是位置信息 ,不能被当作普通的整形看待。
下面我们来看一下status如何得到 退出状态
和 终止信号
终止信号(退出码)
: status>>8&0xFF
退出状态(正常退出还是异常退出)
: status&0x7F
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
通过宏来拿到子进程的退出信息
WIFEXITED(status):
若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status):
若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
阻塞等待
表示的是当父进程执行到waitpid函数时,如果子进程还没有退出,父进程就只能阻塞在waitpid函数,直到子进程退出,父进程通过waitpid读取退出信息后接着执行后面的语句。
非阻塞等待
表示父进程执行到waitpid函数时,如果子进程未退出,父进程会直接读取子进程的退出状态并返回,然后接着执行后面的语句,不会等待子进程的退出。由于非阻塞等待不会等子进程退出,所以我们需要以轮询的方式来不断获取子进程 的退出信息。
轮询
: 父进程在非阻塞式状态的前提下,以循环的方式对子进程进行进程等待,直到子进程退出。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
具有六种以exec
开头的函数,统称为exec函数:
#include
extern char **environ;
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[]);
//系统调用
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数提供了一个在进程中启动另一个程序的执行方法,可以根据指定的文件名或者目录名找到可执行程序,并用它来取代原调用进程的数据段,代码段和堆栈段。在执行完之后,原调用进程,的内容除了进程ID外,其他的内容全部被新的进程替换了。
关于这些函数,我们需要注意几点:
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回
-1
- 所以exec函数只有出错的返回值而没有成功的返回值。(这是因为exec函数调用成功之后,exec函数之后的代码就不会再执行了,所以exec函数调用成功之后返回值没有任何的意义,因此,需要调用失败时的返回值)
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l
(list) : 表示参数采用列表v
(vector) : 参数用数组p
(path) : 有p自动搜索环境变量PATHe
(env) : 表示自己维护环境变量
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
前几个函数没有传递环境变量,但是子进程依然能够通过environ拿到环境变量,是通过进程地址空间的方式让子进程拿到的。
int execle(const char *path, const char *arg,..., char * const envp[]);
这个函数以及下一个我们需要讲解的函数中最后一个'e'
代表的是环境变量和argv一样,我们可以显式的初始化envp
(指针数组)来传递我们的系统环境变量,当然也可以传递我们的自定义的环境变量。
传递系统环境变量
这里我们可以看到,如果我们传递的是系统环境变量,所以系统环境变量被打印了出来了,那么如果我们传递的是自定义的环境变量呢?
这里我们可以看到我们仅仅获取了自定义的环境变量MYENV
,而系统环境变量PATH则获取失败了,如果我们想要同时获取自定义环境变量和系统环境变量该如何做呢?在这里我们就可以使用 putenv
将自定义环境变量导入系统环境变量。然后通过传递系统环境变量environ来实现:
int execvpe(const char *file, char *const argv[],char *const envp[]);
#include
#include
#include
#include
#include
#include
#define NUM 1024 //一个命令的最大长度
#define OPT_NUM 64 //一个命令的做多选项
char lineCommand[NUM];
char* argv[OPT_NUM];
int EXIT_CODE; //保存进程退出码
int main() {
while(1) {
//输出提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);
//获取输入
char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin); //最后留一个位置来存放极端情况下的\0
if( ret == NULL ) {
perror("fgets");
exit(1);
}
//消除命令行中最后的换行符
lineCommand[strlen(lineCommand) - 1] = '\0';
//将输入解析为多个字符串存放到argv中,即字符串切割
argv[0] = strtok(lineCommand, " ");
int i = 1;
//ls颜色显示
if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)
{
argv[i++] = (char*)"--color=auto";
}
while(argv[i++] = strtok(NULL, " "));
//cd改变父进程工作路径
if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)
{
if(argv[1] != NULL)
chdir(argv[1]); //myargv[1]中保存着指定路径
continue;
}
//处理echo内建命令
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)
{
if(strcmp(argv[1], "$?") == 0){ //echo $?
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
} else { //echo $变量
printf("%s\n", argv[1]+1);
}
continue;
}
//创建子进程
pid_t id = fork();
if(id == -1) {
perror("fork");
exit(1);
} else if (id == 0) { //子进程
int ret = execvp(argv[0], argv); //进程程序替换
if(ret == -1) {
printf("No such file or directory\n");
exit(1);
}
} else { //父进程
int status = 0;
pid_t ret = waitpid(id, &status, 0); //进程等待
EXIT_CODE = (status >> 8) & 0xFF; //获取退出状态
if(ret == -1){
perror("wait");
exit(1);
}
}
}
return 1;
}