Linux——进程控制
文章目录
一、进程创建
上篇文章中我们已经了解到,创建一个进程可以调用fork()函数
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
fork调用失败的原因:
1. 系统中有太多的进程
2. 实际用户的进程数超过了限制
进程调用fork,内核会做以下操作:
1. 分配新的内存块和内核数据结构给子进程
2. 将父进程部分数据结构内容拷贝至子进程
3. 添加子进程到系统进程列表当中
4. fork返回,开始调度器调度
也就是父进程调用fork函数,操作系统会将父进程的代码和数据共享给子进程,并且按照父进程的PCB拷贝一份给子进程(特殊值,例如PID子进程还是自己的)
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,就会以写时拷贝的方式各自一份副本,如下图所示
fork常规用法:
1. 一个父进程希望复制自己,通过fork的返回值进行分流,使父子进程同时执行不同的代码段
例如:父进程等待客户端请求,生成子进程来处理请求
2. 一个进程要执行一个不同的程序
例如:子进程从fork返回后,调用exec函数
关于fork的第一种用法,fork如何创建子进程,写实拷贝等概念,上篇文章进程概念中详解了
点击此处传送门:进程概念中篇
此篇文章主要讲述第二种方法 也就是 进程替换
二、进程终止
2.1 退出码
我们在平时写main函数的时候,总会写上return 0;
为什么?
main函数其实也是函数,是函数就会被调用执行,而如何被执行呢?
现在我们已经很清楚了,将其代码和数据加载到内存,并且建立对应的PCB,也就是创建一个进程
而子进程必然是父进程出来的,父进程为什么要创建子进程呢?
父进程创建子进程必然是为了让子进程执行某些任务
既然父进程创建子进程是为了让子进程执行某些任务,那么父进程肯定需要知道任务完成的怎么样,如何知道呢?
每个进程结束的时候都会返回退出码,退出码用来显示子进程的运行结果
main函数中的return 0;
0 就是退出码
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果
退出码的作用:
标定进程结束时程序是否正确
获取最近一个执行完毕进程的退出码:
echo $?
我们用0表示成功,非0表示失败。而非0的不同数字表示不同的错误
C语言的错误码 errno
错误码通常是衡量一个库函数或者是一个系统调用人个函数的调用情况
退出码通常是一个进程退出的时候,他的退出结果
strerror
可以通过标准错误的标号,获得错误的描述字符串 ,将单纯的错误标号转为字符串描述,方便用户查找错误
我们将其中的内容打印出来
2.2 进程退出场景
进程退出场景一般有三种:
进程出异常,本质是进程收到了对应的信号,自己终止了
所以,一个进程是否出异常,我们只要看有没有收到信号即可
查看进程退出信号:
kill -l
总结:
进程退出分为两种:
1. 代码跑完进程正常退出
2. 代码没跑完异常退出
正常退出可以查看进程退出码,异常退出时退出码无意义,需要看信号
所以进程退出只需要看两个数字——退出码与信号
2.3 进程常见退出方法
正常终止(也就是上图中的代码跑完了,可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit()
3. _exit()
异常退出:
ctrl + c,信号终止
exit和_exit的区别
1. exit是库函数,_exit是系统调用,exit()函数本质上是封装了系统接口_exit()
2. exit终止进程的时候,会自动刷新缓冲区,_exit终止进程的时候,不会自动刷新缓冲区
return退出
return是一种更常见的退出进程方法,执行
return n;
等同于执行exit(n);
因为调用main的运行函数会将main的返回值当做exit的参数
三、进程等待
3.1 进程等待的必要性
1. 子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏
2. 进程一旦变成僵尸状态,kill -9 都结束不了进程,因为谁也没有办法杀死一个已经死去的进程
3. 父进程需要知道派给子进程任务执行的结果,子进程是否正常退出,如果正常退出,结果对还是不对
4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
我们创建子进程是为了让它帮助父进程做某些事,所以父进程需要它的执行结果,所以在子进程退出的时候,会变成僵尸进程,把退出结果写入PCB中,父进程来回收以便获得结果(退出状态),但是我们在前面讲过父子进程执行的先后顺序并不确定,所以有可能父进程先退出,造成内存泄漏,所以我们需要让父进程进行进程等待
什么是进程等待?
通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程
为什么要进行等待?
解决子进程僵尸问题带来的内存泄漏问题------目前必须
父进程为什么要创建子进程?要让子进程来完成任务,子进程将任务完成的如何,父进程要不要知道?
要知道,需要通过进程等待的方式,获取子进程退出的信息(两个数字),不是必须的,但是系统需要提供这样的基础功能
3.2 进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait时进程等待能回收子进程僵尸状态,Z->X
如果子进程根本就没有退出,父进程必须在wait上进行阻塞等待直到子进程僵尸,wait自动回收,然后返回
关于阻塞等待 :一般而言,谁先运行不知道,但是最后一般都是父进程最后退出
waitpid方法
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
rid > 0,等待成功
rid == 0,等待是成功的,但是子进程还没有退出
rid < 0,等待失败
参数:
pid:
pid=-1,等待任一个子进程,与wait等效
pid>0,等待其进程ID与pid相等的子进程
status:
WIFEXITED(status)
: 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status)
: 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
options
:
0——阻塞等待(也就是父进程一直在询问子进程是否执行完毕)
WNOHANG——非阻塞等待(若pid指定的子进程没有结束,则waitpid()函数返回0,父进程执行自己的代码,做自己的事,过一段时间再询问子进程,也就是非阻塞轮询等待,若正常结束,则返回该子进程的ID)
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
根据上图中status结构的分析,我们可以通过对status进行逻辑操作和位运算操作拿到退出码和信号
printf("exit signal:%d, exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff));
Linux给我们提供了两个宏函数,更加方便的让我们知道程序是否正常退出,并且拿到正常退出时的退出码
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
注意:
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
如果不存在该子进程,则立即出错返回
父进程如何得知子进程的退出信息呢? 通过wait/waitpid?
子进程退出的时候,要修改状态Z,并将子进程的退出信号和退出码写入自己的PCB中
此时父进程在调用wait/waitpid等待子进程,OS就会执行*statusp = (exit_code<<8) | exit_siganl;
将PCB中的退出码和信号写入变量status中,通过输出型参数返回给父进程
我们为什么不用全局变量获取子进程的退出信息? 而用系统调用?
进程具有独立性,父进程无法直接获得子进程的退出信息
四、进程替换
这里讲述的就是fork的第二种用法:一个进程要执行一个与父进程不同的程序
4.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(可以通过fork返回值进行判断分流,执行不同的代码分支)
如果我们想让子进程执行新的程序呢? 执行全新的代码和访问全新的数据,不再和父进程有瓜葛
程序替换
原理:
子进程调用一种exec函数以执行另一个程序
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,执行的是新进程的代码和数据
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
由于是替换原来进程的代码和数据,如果替换成功,那么在原来进程后面的代码就不会执行,只会执行新进程的代码和数据
子进程怎么知道要从新的程序的最开始执行? 它怎么知道最开始的地方在哪里?
程序计数器,pc. eip
替换的代码和数据中有新程序的可执行入口地址,将他们加载到寄存器中
4.2 替换函数
如上图,有六种以exec开头的函数,统称exec函数,前五个是三号手册,最后一个是二号手册
所以前五个都是库函数,而第六个是系统接口,前五个本质上都是第六个封装而来
为了使用者有更好的体验,满足更多的调用场景,所以设计了多个适用调用接口
这些函数原型看起来很容易混,但有一定的规律
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,并且不再返回
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值
这里我们调用第一个函数execl
来验证一下程序替换
执行程序
我们可以很明显的看到在执行程序替换后,原来程序下面的printf并没有执行,只执行了替换之前的printf
execl就是程序替换函数,execl执行完后,代码全部被覆盖,开始执行新程序的代码了,所以最后一个printf无法执行
4.3 替换细节
当我们进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的,如何验证呢?
进程的创建方式,就决定了一定拥有父子进程之间的关系,父进程创建子进程,子进程会与父进程共享代码和数据,子进程的PCB会按照父进程的进行拷贝,其中就有父进程的环境变量
环境变量被子进程继承下去是一种默认行为,不受程序替换的影响,为什么?
通过地址空间可以让子进程继承父进程的环境变量数据
程序替换只替换新程序的代码和数据环境变量不会被替换
如果不传环境变量,父进程的环境变量会原封不动传递给子进程,可以直接用,但也可以手动将父进程环境变量传给子进程
char** environ
是一个默认的全局的指针,指向环境变量表char *argv[]
同样在进程覆盖的时候,如果我们不调用有环境变量的接口,子进程的环境变量和父进程相同,替换不会改变环境变量
如果我们想传递我们自己的环境变量,我们可以直接构造环境变量表,调用接口,给子进程传递(覆盖传递)
如果我想新增传递呢?
int putenv(char *string);
可以把自定义环境变量导入到调用进程的环境变量中,这样先在父进程中添加环境变量,再传给子进程,就能做到新增传递了
五、实现简易版myshell
此处细节不做详细解释
主要分为四步:
1. 打印提示符并且获取用户命令字符串
2. 分割命令字符串
3. 判断是否是内建命令,如果是则执行(内建命令本质是shell内部的函数,用子进程的替换进程的方式无法完成任务,需要shell自己执行)
4. fork创建子程序,进行程序替换,执行对应的命令
代码如下:
注意:由于是简易版myshell,加上个人能力有限,可能存在BUG
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAXCMD 1024
#define MAXARGC 64
int lastcode = 0;
char pwd[1024];
char* enval[1024];
int encount = 0;
const char* getUser()
{
const char* user = getenv("USER");
if(user)
{
return user;
}
else
{
return "none";
}
}
const char* getHOSTNAME()
{
const char* name = getenv("HOSTNAME");
if(name)
{
return name;
}
else
{
return "none";
}
}
const char* getPwd()
{
const char* pwd = getenv("PWD");
if(pwd)
{
return pwd;
}
else
{
return "none";
}
}
int getUsercommand(char* command, int num)
{
printf("[%s@%s %s]# ",getUser(),getHOSTNAME(),getPwd());
char* s = fgets(command,num,stdin);
if(s == NULL)
{
return -1;
}
else
{
command[strlen(command)-1] = '\0';
return strlen(command);
}
}
void commandSplit(char* in, char* out[])
{
int cnt = 0;
out[cnt++] = strtok(in," ");
while(out[cnt++] = strtok(NULL," "));
// for(int i = 0; out[i]; i++)
// {
// printf("%s ",out[i]);
// }
}
int exeCute(char* argv[])
{
int n = fork();
if(n < 0)
{
return -1;
}
else if(n == 0)
{
execvp(argv[0],argv);
exit(1);
}
else
{
int status;
pid_t pid = wait(&status);
if(pid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
}
void cd(const char* path)
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(pwd,"PWD=%s",tmp);
putenv(pwd);
}
int builtCommand(char* argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
char* path = NULL;
if(argv[1] == NULL)
{
path = NULL;
}
else
{
path = argv[1];
}
cd(path);
return 0;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL)
{
return 0;
}
strcpy(enval[encount],argv[1]);
putenv(enval[encount++]);
}
else if(strcmp(argv[0],"echo") == 0)
{
if(argv[1] == NULL)
{
return 0;
}
if(*argv[1] == '$')
{
if(strcmp(argv[1]+1,"?") == 0)
{
printf("%d\n",lastcode);
lastcode = 0;
}
else
{
printf("%s\n",getenv(argv[1]+1));
}
return 0;
}
else
{
for(int i = 1;argv[i];i++)
{
printf("%s ",argv[i]);
}
printf("\n");
return 0;
}
}
}
int main()
{
while(1)
{
char command[MAXCMD];
char* argv[MAXARGC];
int n = getUsercommand(command,sizeof(command));
if(n <= 0)
{
continue;
}
commandSplit(command,argv);
n = builtCommand(argv);
if(n == 0)
{
continue;
}
exeCute(argv);
}
return 0;
}