一、进程创建
1. fork函数初识
Linux中的fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>//头文件
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,由于写时拷贝的发生,所以地址一样,但是内容不一样,因此 就可以让父子进程执行不同的代码了。
2. 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
3. fork常规的用法
💕 fork的常规用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
💕 fork调用失败的原因:
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二、进程终止
1. 进程退出码
我们创建进程的目的是为了让进程帮我们执行任务,但是任务执行的结果如何应该如何衡量呢?如果一个程序正常运行完了,那他会有两种结果:
结果正确
或者结果不正确
,一般来说,不同的退出码表示的是不同的结果。
一般来说:0表示运行结果正确,非0表示运行结果不正确。这里我们来看一下C语言中的库函数strerror
,他表示从内部数组中搜索错误号errnum,然后返回一个指向错误消息的字符串的指针。
在Linux下,我们可以使用echo $?
来查看进程退出码,但在这里我们需要注意的是它==只能查看最近的一个进程的进程执行完成时的退出码。==
2. 进程退出场景及常见的退出方法
💕 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
💕 进程退出的方法
进程正常退出有如下几种方法:
- 从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、空指针野指针等问题。
例如,当我们在程序中使用一个整数去除零的时候:
三、进程等待
进程等待==指的是通过系统调用获取子进程退出码或者退出信号的方法。==
1. 进程等待的必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的本质就是父进程需要通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。
2. 进程等待的方法
wait方法
如果等待进程终止成功,wait函数将会返回终止进程的id值,等待失败则会返回-1,下面我们来看一下wait函数。
我们先来看一下wait的用法:
子进程在执行完之后的五秒内由于父进程处于休眠状态,所以子进程正在处于僵尸状态,五秒过后父进程调用wait回收子进程,子进程由僵尸状态退出结束了进程。最后父进程在等待五秒后自动退出。
waitpid方法
如果我们将最后一个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非零,提取子进程退出码。(查看进程的退出码)
3. 非阻塞等待
阻塞等待
表示的是当父进程执行到waitpid函数时,如果子进程还没有退出,父进程就只能阻塞在waitpid函数,直到子进程退出,父进程通过waitpid读取退出信息后接着执行后面的语句。
非阻塞等待
表示父进程执行到waitpid函数时,如果子进程未退出,父进程会直接读取子进程的退出状态并返回,然后接着执行后面的语句,不会等待子进程的退出。==由于非阻塞等待不会等子进程退出,所以我们需要以轮询的方式来不断获取子进程 的退出信息。==
轮询
: 父进程在非阻塞式状态的前提下,以循环的方式对子进程进行进程等待,直到子进程退出。
四、进程程序替换
1. 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2. 替换函数
具有六种以exec
开头的函数,统称为exec函数:
#include <unistd.h>
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) : 表示自己维护环境变量
execlp
int execlp(const char *file, const char *arg, ...);
execv
int execv(const char *path, char *const argv[]);
execvp
int execvp(const char *file, char *const argv[]);
前几个函数没有传递环境变量,但是子进程依然能够通过environ拿到环境变量,是通过进程地址空间的方式让子进程拿到的。
execle
int execle(const char *path, const char *arg,..., char * const envp[]);
这个函数以及下一个我们需要讲解的函数中最后一个'e'
代表的是环境变量和argv一样,我们可以显式的初始化envp
(指针数组)来传递我们的系统环境变量,当然也可以传递我们的自定义的环境变量。
传递系统环境变量
这里我们可以看到,如果我们传递的是系统环境变量,所以系统环境变量被打印了出来了,那么如果我们传递的是自定义的环境变量呢?
这里我们可以看到我们仅仅获取了自定义的环境变量MYENV
,而系统环境变量PATH则获取失败了,如果我们想要同时获取自定义环境变量和系统环境变量该如何做呢?在这里我们就可以使用 ==putenv
将自定义环境变量导入系统环境变量。然后通过传递系统环境变量environ来实现:==
execvpe
int execvpe(const char *file, char *const argv[],char *const envp[]);
五、shell的模拟实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#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], "{
mathJaxContainer[0]}?
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;
}