在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
//fork函数给子进程返回0,给父进程返回子进程
//的pid,出错返回-1
pid_t id = fork();
父进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:
1、分配新的内存块和内核数据结构给子进程。
2、将父进程部分数据结构内容拷贝至子进程。
3、添加子进程到系统进程列表当中。
4、fork返回,开始调度器调度。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将开始它们自己的进程,看如下程序。
这里看到了三行输出,一行before,两行after。进程19654先打印before消息,然后它又打印after。另一个after消息由19655打印的。注意到进程19655没有打印before,为什么呢?
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1、子进程返回0,
2、父进程返回的是子进程的pid。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
这里有一个注意的点就是:当有进程对数据进行修改时,如果操作是合法的,会发生写时拷贝,(在创建子进程时,父子进程页表中对应这个数据的读写权限都被操作系统设置成了只读权限)操作系统重新在物理内存中开辟空间,把物理地址填充到页表中,然后再对该数据进行修改。为什么要在创建子进程的时候就把该数据的权限设置成只读呢?因为如果数据只读,说明该数据是不可被修改的,此时无论是父进程还是子进程来修改数据,都会被操作系统拦截,然后操作系统判断该数据原本的读写权限,如果该数据原本是可读写的,那么此时操作系统做写时拷贝的工作,如果该数据原本就是只读的,那么操作系统会把这个行为识别成非法操作,直接拦截,即不会发生写时拷贝。所以必须要先把父子进程的页表的对应该数据的权限变为只读,才能触发操作系统判断该操作的合法性,从而做出写时拷贝或者拦截的动作,至于操作系统为什么不是直接判断,而是先变成只读,然后再判断原来的,因为一定要有一个触发操作系统拦截,进行判断的地方。
1、一个父进程复制自己,使父子进程同时执行不同的代码块。例如,父进程等待设备内容就绪,生成子进程来处理内容。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数进行程序替换。
1、系统中有太多的进程。
2、实际用户的进程数超过了限制。
1、代码运行完毕,结果正确。
2、代码运行完毕,结果不正确。
3、代码异常终止。
main函数的返回值,本质是表示:进程运行完成时是否是正确的结果,如果不是,可以用不同的数字,表示不同的出错原因。
echo $?//打印最近一次进程结束的退出码
#include
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
该函数是系统调用接口,只终止进程,不做资源的回收工作。
#include
void exit(int status);
exit是库函数,封装了_exit系统调用接口,即exit函数最后也会调用_exit接口,但是在调用_exit函数之前,还做了其他工作:
return是一种更常见的退出进程方法。在main函数中执行return n等同于执行exit(n),因为在main函数中使用return返回时会将main的返回值当做 exit的参数,然后调用exit函数。但是只有在main函数中使用的return才会使进程调用exit函数结束进程,在其它的函数中的return只代表该函数调用结束,进程继续执行后续代码。
父进程通过调用系统调用接口wait/waitpid,对子进程进行状态检测与回收的行为叫做进程等待。
(1)子进程结束后会变成僵尸进程,如果父进程没有对其进行回收,即父进程没有对子进程进行进程等待,那么子进程就会一直保持着僵尸状态,该子进程的各种内核数据结构(PCB,进程地址空间,页表等)也就不会被释放,进而导致内存泄漏。并且僵尸进程是已经死亡的进程,就算用kill -9也无法杀掉它,因为你无法杀掉一个已经死掉的进程,所以必须要通过进程等待的方式回收释放僵尸进程的资源。
(2)我们要通过进程等待,获取子进程的退出情况。如:子进程是否出现异常,如果异常,异常的原因是什么,如果没有异常,那么运行的结果是否正确,如果不正确,原因是什么等,都是父进程可能会关心的事情。当然,父进程可以选择不关心,但是父进程想关心这些情况的时候必须要有渠道获得这些信息,进程等待就能做到。
综上,父进程对子进程进行进程等待是很有必要的。
可以看到子进程运行5秒后退出,状态变为Z,即僵尸进程。父进程再运行2秒后对子进程进行进程等待,回收资源。
下面就来介绍一下wait系统调用函数。
#include
#include
pid_t wait(int*status);
返回值:
成功时返回被等待进程pid,失败时返回-1
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
wait函数默认是等待任意一个子进程的,等待一个子进程成功后就会返回该子进程的pid,如果等待失败就会返回-1。
这里的输出型参数status是表示进程的退出状态的,这个整形是由几部分构成的,一个int是32个比特位,但是这个status只用了低16个比特位,然后低16位中的高8位表示退出码,低7位表示退出信号,第8位是一个coredump标志位。如下图所示:
当我们没有收到退出信号时,即退出信号为0时,表示进程正常退出,此时退出码表示的就是我们进程正常结束时的结果是否正确,退出码为0表示结果正确,退出码不为0,那么该退出码会对应自己的退出码描述,表示错误原因。
当我们收到退出信号,即退出信号不为0时,说明我们的进程是异常终止的,此时的退出信号对应着进程异常退出的原因,注意,进程异常退出时,因为代码都没有跑完,所以进程的退出码就没有意义了,只有在进程正常退出的时候退出码才有意义。
退出码以及对应的退出码描述:
退出信号以及信号对应的描述:
那么如何拿到退出码和退出信号呢?通过位运算就可以了。
关于如何进行位运算拿到退出码和退出信号,库里面提供了两个宏,方便我们取到退出码和判断进程是否异常退出。
分别是:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真,否则为假。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED为真,提取子进程退出码。(查看进程的退出码)
退出信号必须要用位运算的方式拿到,库里面没有提供对应的宏获取退出信号。代码如下;
如果子进程不退出,父进程调用wait进行进程等待的时候,也就不返回,默认叫做阻塞等待。等到子进程退出时,操作系统就会对子进程的PCB中的该进程的状态设置为Z(僵尸)状态,然后父进程检测到子进程的状态是Z状态就会对该子进程进行等待,读取子进程的退出信息,并释放子进程的内核数据结构对象(PCB等)。
waitpid方法和wait方法是非常一样的,只是多了两个参数。
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待:
如何取出status中的退出码和退出信号在上面已经谈过了,这里就不再多说。
那么有一个问题来了,那就是你说通过这个status就能拿到子进程的退出信息(退出码和退出信号),那么我就不能用一个全局变量记录吗?子进程退出时把这个全局变量status通过位操作设置退出码和退出信号,然后父进程读取这个status不就拿到了子进程的退出信息了吗?为啥还一定要调用wait和waitpid这样的函数那么麻烦呢?
不能用全局变量记录的原因是什么?原因就是进程具有独立性,子进程对数据进行写入操作的时候会发生写时拷贝,所以父进程根本就看不到子进程修改后的status,也就没办法获得子进程的退出信息。所以我们只能调用系统调用接口,让操作系统帮父进程拿到子进程的退出信息。
那么子进程的退出信息是在哪里保存的呢?就在子进程的PCB内核数据结构中保存着,所以在父进程wait等待子进程之前,子进程的PCB是需要一直维护着的,子进程的代码和数据那些可以先释放,但是PCB一定要等父进程wait/waitpid之后再释放,因为父进程需要知道子进程的退出信息。
所以进程等待的本质就是父进程读取子进程的内核数据结构对象,获取子进程的退出信息,并释放子进程的内核数据结构(PCB等)。
调用waitpid系统调用函数等待成功时返回被等待进程pid,失败时返回-1(等待一个不是自己的子进程时就会等待失败)
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
库函数:
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 *filename, char *const argv[],
char *const envp[]);
1、这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
2、如果调用出错则返回-1。
3、所以exec函数只有出错的返回值而没有成功的返回值。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。其它的都是库函数,在其它的这几个库函数中都封装了系统调用函数execve。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
int quit = 0;
//最近一次进程的退出码
int lastcode = 0;
//命令行参数的最大长度
#define LINE_SIZE 1024
//命令行字符数组的长度
#define ARGC_SIZE 32
//输入的命令行
char CommandLine[LINE_SIZE];
//当前路径
char pwd[LINE_SIZE];
//字符串指针数组
char* argv[ARGC_SIZE];
//命令的分隔符
#define SPLIT " "
//自定义环境变量表
char myenv[LINE_SIZE];
//把整体输入的命令分离成一个一个的命令
int splitstring(char* cline)
{
int i = 0;
argv[i] = strtok(cline, SPLIT);
if (argv[i] == NULL)
{
return 0;
}
i++;
//最后一次会把NULL赋值给argv[i]
while (argv[i++] = strtok(NULL, SPLIT));
//for(argv[i]=strtok(cline,SPLIT);strtok(NULL,SPLIT);i++)
//{}
return i - 1;
}
//获取当前路径
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
//输入命令行
void interact(char* cline, int sz)
{
getpwd();
printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), pwd);
char* s = fgets(cline, sz, stdin);
assert(s);
(void)s;
cline[strlen(cline) - 1] = '\0';
//cout<
}
//内建命令
//需要父进程自己执行
int BuildCommand(int _argc, char* _argv[])
{
if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
{
chdir(_argv[1]);
//更改路径后需要重新刷新
getpwd();
//修改环境变量表中的PWD
sprintf(getenv("PWD"), "%s", pwd);
lastcode = 0;
return 1;
}
if (_argc == 2 && strcmp(_argv[0], "export") == 0)
{
//这里必须要自己开一个数组存储导出的环境变量
//不然等到下一次运行别的命令的时候该导出的环境变量会被覆盖
//详情可以看后面的图片分析
strcpy(myenv, (char*)_argv[1]);
//到处环境变量
putenv(myenv);
//设置退出码
lastcode = 0;
return 1;
}
if (strcmp(_argv[0], "ls") == 0)
{
//"--color"是使我们的不同文件有不同的配色
_argv[_argc++] = (char*)"--color";
//指令要以NULL结尾
_argv[_argc] = NULL;
return 0;
}
if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
{
if (strcmp(_argv[1], "$?") == 0)
{
cout << lastcode << endl;
lastcode = 0;
}
else if (*_argv[1] == '$')
{
char* p = (char*)_argv[1] + 1;
//获取环境变量
char* val = getenv(p);
if (val != NULL)
{
//打印环境变量
printf("%s\n", val);
}
lastcode = 0;
}
return 1;
}
return 0;
}
void NormalExcute()
{
//普通命令
//创建子进程进行程序替换执行
pid_t id = fork();
if (id < 0)
{
perror("fork fail");
lastcode = 1;
exit(1);
}
if (id == 0)
{
//子进程
execvp(argv[0], argv);
lastcode = 2;
exit(2);
}
else
{
//父进程等待
int status = 0;
pid_t childpid = waitpid(-1, &status, 0);
if (childpid < 0)
{
perror("wait fail");
lastcode = 3;
exit(3);
}
else if (childpid == id)
{
cout << "father wait success!" << endl;
if (WIFEXITED(status))
{
cout << "子进程正常退出;" << "退出码:" << WEXITSTATUS(status) << endl;
lastcode = WEXITSTATUS(status);
}
else
{
cout << "子进程异常退出;" << "退出信号:" << (status & 0x7F) << endl;
}
}
}
}
int main()
{
while (!quit)
{
//交互问题,获取命令行
interact(CommandLine, sizeof(CommandLine));
//分割字符串
int argc = splitstring(CommandLine);
if (argc <= 0)
{
continue;
}
//判断是否为内建命令
//不能用sizeof(argv),否则会越界
int ret = BuildCommand(argc, argv);
if (!ret)
{
//普通命令
NormalExcute();
}
}
return 0;
}
应该做以下修改:
以上就是今天想要跟大家分享的全部内容了,你学会了吗?如果对你有所帮助,那么就点点赞点点关注呗!后期还会持续更新Linux系统的相关内容哦,我们下期见!!!