本文将系统介绍进程控制的基本要素,包括进程创建, 进程终止, 进程等待, 进程替换等方面。深入理解进程创建的相关知识, 帮助更好的构建知识架构!
博客主页:酷酷学!!!感谢关注!
在linux中fork函数非常重要, 它从已存在的进程中创建一个新的进程, 新进程为子进程,而原进程为父进程。
#include<unistd.h>
pid_t fork(void);
返回值:子进程中返回0, 父进程中返回子进程id, 出现错误则返回-1
进程调用fork,当控制转移到内核中的fork代码后, 内核会做下面几件事情:
当一个进程调用fork()之后,就有两个二进制代码相同的进程。而且他们都运行到相同的地方。但每个进程都将可以开始他们自己的旅程。
这里我们看到了三行输出, 一行before, 两行after,父进程先打印before消息,然后它又打印after,另一个after消息有子进程打印的,但是子进程没有打印before,为什么呢?
所以, fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行,注意,fork之后,谁先执行由调度器决定。
子进程返回0,父进程返回的是子进程的pid。
为什么有两个返回值, 因为fork之后是两个不同的进程, 而返回值也是给不同的进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 执行 fork()
if (pid < 0) { // 处理 fork 失败的情况
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) { // 子进程执行
printf("This is the child process (PID: %d)\n", getpid());
} else { // 父进程执行
printf("This is the parent process (PID: %d, Child PID: %d)\n", getpid(), pid);
}
return 0; // 所有进程执行完毕
}
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,如下图:
所以数据不修改时,代码共享是因为子进程拷贝了父进程的mm_struct和页表,物理内存是一样的。
那系统是怎么知道要写时拷贝的呢具体是怎么做到呢?
首先第一步, 系统会在一开始就把权限设置为了只读权限, 如果要发生写入,则会引发系统错误, 导致缺页中断, 这是系统会进程判断, 如果是要发生写时拷贝,则系统会申请内存,然后进行拷贝一份,再修改子进程的页表将物理地址修改为实际的物理地址,然后再恢复到只读权限,你的写入操作不是目标区域进行覆盖。比如count++。
一个父进程希望复制自己,使父子进程同时执行不同的代码段, 例如, 父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序, 例如子进程从fork返回后,调用exec函数。
本质:释放系统资源, 就是释放进程申请的相关内核数据结构和对应的代码和数据。
除了main函数的返回值表示进程结束,其它函数的return都表示函数结束。
main函数的返回值是返回给父进程或者系统的,命令行中获取最近一个进程的返回值我们可以使用echo $?来获取, 如下图所示:
对于返回值,0表示成功, 非0表示错误,为什么会失败呢?系统提供了不同的错误码信息记录了错误的原因, 也可以自己约定错误码。
那么什么是错误码呢? 举个例子:
如果想要查看错误码, 我们可以使用errno函数, 使用man可以查看命令详情。
如果想知道具体的错误内容, 可以使用strerror函数,参数传递错误码。
举个例子:
我们可以将错误码和对应的错误信息进行打印:
例如:
在代码的任何地方, 让进程直接结束。参数就是返回的错误码。
如果使用exit, 如果缓冲区有数据, 则会被刷新出来。
是系统层的进程终止调用,
如果使用_exit, 缓冲区的数据则不会被刷新出来。
exit属于是语言级别的,在三号手册, 而_exit是系统级别的,在二号手册。
_exit本质上是系统调用
所以我们上面的_exit实际上是绕过了语言层, 直接进行了系统调用, 而刚刚的缓冲区是语言级别的, fflush也是语言级别的。
首先我们可以查看一下fork的返回值, 如果fork失败, 则错误码会被设置。
一般而言, 父进程创建的子进程, 父进程就要等待子进程进行回收, 如果子进程一直不退出, 则父进程就会阻塞在wait内部。
wait的作用,等待任意的子进程(参数可以传nullptr表示不获取status)
常用: waitpid的作用:第一个参数 pid>0表示等待指定的一个进程,pid == -1表示等待任意一个子进程
看一下他们的返回值, 如果等待成功则返回对应的子进程,如果等待失败则返回-1.
举个例子:
等待任何一个子进程
当然, 我也可以修改id的参数,比如更换为刚刚子进程id,这里就不展示了。
waitpid的第二个参数,它会帮助父进程获取子进程的退出信息,通过参数的方式给我们带出来。输出型参数。
但是这里的退出信息却是256,为什么不是1呢?
其实,status这个参数包含的信息并不只是退出码,它的本质是一个位图, 它的结构中前八位是退出状态,有256种状态 ,低七位是终止信号, 还有一个标志位。
如果想要提取退出状态则需要进行位运算,如下
小问题: 那我们可不可以不使用status来获取状态码,而是用一个全局变量呢?
不可以, 因为进程具有独立性,子进程修改父进程看不到,会发生写时拷贝。
进程退出分为三种:
如果进程没有异常,则终止信号是0,所以上面出现的256就得到解释了,低七位都是0。
终止信号:
也可以进行终止信号的提取:
实例,让子进程对数据进行备份
其中WEXISTATUS这个宏 可以直接让我们从错误信息中提取出退出码。
waitpid的第三个参数就是关于阻塞等待与非阻塞等待
首先waitpid的返回值, 如果>0表示,返回目标进程pid, 如果 == 0, 等待成功,但是子进程没有退出, <0 等待失败.
参数如果传0表示阻塞等待, 如果传WNOHANG表示非阻塞等待。
举个例子:
fork()之后, 父子各自执行父进程代码的一部分如果子进程想执行一个全新的程序呢? 进程的程序替换来完成这个功能!
程序替换是通过特定的接口, 加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用⼀种exec函数以执行另⼀个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
那么下面就让我们看看有哪些调用接口。
其实有七种调用接口, 六种是c标准库提供的, 还有一个是系统层的
#include <unistd.h>
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 execve(const char *path, char *const argv[], char *const envp[]);
举个例子:
这些函数原型看起来很容易混, 但只要掌握了规律就很好记。
• l(list): 表示参数采用列表 • v(vector): 参数用数组 • p(path): 有p自动搜索环境变量PATH • e(env): 表示自己维护环境变量
示范:
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
最后推荐两本书籍:
《深入理解计算机系统》与《程序员的自我修养》
进程控制是操作系统中的一个重要主题,主要涉及如何管理和调度进程以确保计算机系统的高效运行。
如果感觉本篇博客对你有帮助的话就点个关注吧~