进程创建
1.1fork函数初识
在linux中fork函数是⾮常重要的函数 ,它从已存在进程中创建⼀个新进程。新进程为⼦进程 ,⽽原进 程为⽗进程。
#include <unistd.h>
pid_t fork(void);
//返回值: ⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调⽤fork , 当控制转移到内核中的fork代码后 ,内核做:
- 分配新的内存块和内核数据结构给⼦进程
- 将⽗进程部分数据结构内容拷贝⾄⼦进程
- 添加⼦进程到系统进程列表当中
- fork返回 ,开始调度器调度
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这⾥看到了三⾏输出,⼀⾏before,两⾏after。进程43676先打印before消息,然后它有打印after。 另⼀个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所⽰
所以,fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完全由调度器决定。
1.2fork函数返回值
- ⼦进程返回0,
- ⽗进程返回的是⼦进程的pid。
- 错误返回-1
1.3 写时拷⻉
最后一节
当父进程没有创建子进程时,数据段是可读写,当创建了子进程且就变成了只读,之后子进程有了更改,就会报错引发OS进行写实拷贝
1.4 fork常规⽤法
- ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求, ⽣成⼦进程来处理请求。
- ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。
1.5 fork调⽤失败的原因
系统中有太多的进程(排队的太多了)
实际⽤⼾的进程数超过了限制(系统规定用户建立的进程是有数的)
2.进程终⽌
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1 进程退出场景
- 代码运⾏完毕,结果正确
- 代码运⾏完毕,结果不正确(抛出退去码)
- 代码异常终⽌
2.2 进程常⻅退出⽅法
正常终⽌(可以通过 echo $? 查看进程退出码):
从main返回
调⽤exit
_exit
异常退出:
ctrl + c,信号终⽌
写一个小例子:我们随意打开一个不存在的文件,之后在关闭
上面知道代码运⾏完毕,结果正确,你知道是对的,没结果时你怎么知道你的程序运行结果是对还是错?
main函数的返回值,其实是进程退出时的退出码 当错误时打印的是一个程序(进程)最近的退出码,并写到task_struct内部。
2.2.1进程退出码
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令
是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。
代码 1 或 0 以外的任何代码都被视为不成功。
- 退出码 0 表⽰命令执⾏⽆误 ,这是完成命令的理想状态。
- 退出码 1 我们也可以将其解释为 “ 不被允许的操作”。例如在没有 sudo 权限的情况下使⽤ yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
- 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的 ,它们属于
- 128+n 信号 ,其中 n 代表终⽌码。
- 可以使⽤strerror函数来获取退出码对应的描述。
测试一下Linux有什么退出码
如何知道是什么具体错误呢?
errno:它用于指示在程序运行过程中发生的错误
没有这样的文件或目录
如何自定义退出码
想return 什么就写什么。
_exit函数VS exit函数
_exit(系统提供)
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态 ,⽗进程通过wait来获取该值
说明:虽然status是int ,但是仅有低8位可以被⽗进程所⽤ 。所以_exit(-1)时 ,在终端执⾏$?发现 返回值是255。
exit(C语言库提供)
#include <unistd.h>
void exit(int status);
exit最后也会调⽤ _exit, 但在调⽤ _exit之前 ,还做了其他⼯作:
1. 执⾏⽤⼾通过 atexit或on_exit定义的清理函数。
关闭所有打开的流 ,所有的缓存数据均被写⼊
3. 调⽤ _exit
代码证明
exit那么进程退出时候会进行资源的回收,尤其是会进行缓冲区的刷新
_exit进程退出的时候不会进行缓冲区的刷新,
从exit()和_exit()对比来说,效果是一样的,区别就是在涮缓冲区上。
那缓冲区在哪里呢?
缓冲区是由C语言提供的库缓冲区。
3.进程等待
引言:什么是进程等待
想象有两个小伙伴,一个是 “大强”(父进程 ),一个是 “小强”(子进程 )。大强给小强安排了任务,比如去收集一些石头。
1. 大强想知道小强啥时候把任务完成,就用了个办法:他跟小强说,等你干完活,记得跟我说一声。然后大强就不做别的事了(从运行状态变成阻塞状态 )在那等着小强的消息,把 CPU 让给其他小伙伴(其他就绪进程 )去用。
2.小强呢,就跑去收集石头了。等他把石头收集完(子进程完成任务并终止 ),就赶紧给大强发个信号,说 “我干完啦”。大强收到这个信号后,就从等着的状态(阻塞状态 )醒过来(变成就绪状态 ),然后 CPU 就又可以让大强接着做他后面的事啦,比如看看小强收集的石头合不合格(处理子进程的终止状态 )。
在计算机里,wait
或者 waitpid
这些函数就像是大强用来等小强消息的工具,通过它们,父进程就能等着子进程把活干完,再接着往下走 。
3.1 进程等待必要性
- 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存 泄漏。
- 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也 没有办法杀死⼀个已经死去的进程。
- 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是 不对,或者是否正常退出。
- ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息.
3.2 进程等待的⽅法
3.2.1 wait⽅法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
wait
函数的作用机制
wait
函数是一个系统调用,其作用是使父进程暂停执行,等待其子进程中的任意一个终止。当父进程调用 wait
时,内核会检查是否有已经终止的子进程。如果有,wait
会获取该子进程的终止状态信息,并返回该子进程的进程 ID,父进程继续执行后续代码;如果没有子进程终止,父进程就会被阻塞(进入睡眠状态 S),直到有子进程终止才会被唤醒并继续执行。
第一种 正常,回收⼦进程资源,获取⼦进程退出信息.
wait
函数会阻塞父进程,直到它的一个子进程终止。当子进程终止后,wait
函数返回终止子进程的进程 ID
第二种 如果等待子进程,子进程没有退出,父进程会一直阻塞在wait调用吗?
在同一账户开两个窗口 发现父进程会一直等待
while :; do ps ajx | head -1 && ps ajx | grep wlw5 ; sleep 1;done |grep -v grep
man 2 wait
3.2.2 waitpid⽅法
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:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
获取id的值 ,与wait效果一样
当pid的值>0时, id+1仿造等待其进程ID与pid相等的⼦进程。
接收指定的子进程所以,导致建立的子进程变成僵尸进程🧟♀️
问题一 获取退出码
首先定义了 int status = 0;
用于存储子进程的终止状态信息。然后调用 pid_t rid = waitpid(id, &status, 0);
,其中 id
是要等待的子进程的进程 ID,&status
用于存储子进程的终止状态,0
表示默认的等待选项。
为什么返回的 为什么不是1,而是256呢?
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
⽐特位):
可以把它当成一张位图来看,而其中整形这个变量一共32个比特位,其中高16位,低16位。
我们不管高16位,一个比特位要么0要么1,其中8-15是(退出状态码)后面的8位 全是0一旦后8位!0,异常退出,退出码毫无意义,那256就是2^8二的八次方=256。
的值正常终止的原因,如何获取呢?
(status>>8)&0xFF
:
-
子进程的终止状态信息存储在
status
变量中,它包含了多种信息。在 Linux 中,子进程正常退出时,其退出状态码存放在status
的低 8 位 。 -
status>>8
是将status
的值右移 8 位,这样原本存放在低 8 位的退出状态码就移到了最低 8 位。 -
&0xFF
是进行按位与操作,0xFF
即二进制的11111111
,与右移后的status
进行按位与操作,是为了只保留最低 8 位的值,也就是准确获取子进程的退出状态码 。通过打印这个值,可以知道子进程是以什么状态码退出的,有助于调试和了解子进程的执行结果。
问题二获取代码异常终止信号
所有信号
tatus&0x7F
:这是一个位运算表达式。在 Linux 中,子进程终止状态信息存储在 status
中,其中低 7 位(0x7F
即二进制的 01111111
)用于存储导致子进程终止的信号编号(如果子进程是因信号终止 )。通过与 0x7F
进行按位与操作,可以提取出这部分信号相关信息。如果子进程是正常退出(使用 exit
函数 ),那么这部分值为 0
;如果是因信号(如 SIGTERM
、SIGKILL
等 )终止,就能获取到对应的信号编号。
获取子进程退出码和特殊信号
刚刚在获取子进程推出码和特殊信号时,我们用的是位操作,但计算机里面的或者是它对应的为了配套的操作里面,他不想让你作为未操作提取,所以他给我们提供了若干个宏,这时候看宏它可以帮我们直接去提取
WEXITSTATUS(status)
: 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
WIFEXITED(status):
若为正常终⽌⼦进程返回的状态,则为真,否则异常。(查看进程是否是正常退出)
阻塞调用和非阻塞调用
想象你去餐厅点餐:
-
阻塞调用:就像你点完餐后,站在柜台前一直等着服务员把做好的餐递给你,期间啥也不干,啥也做不了,只能干等着。在这个过程中,你处于 “阻塞” 状态,一直被这件事绊住 。对应到编程里,比如父进程调用
wait
函数等待子进程结束,在子进程没结束前,父进程就卡在那里,不能去做其他事,这就是阻塞调用。
上面的例子都是阻塞调用
-
非阻塞调用:类似你点餐后,没在柜台干等着,而是去旁边找个位置坐下,玩手机或者和朋友聊天。你告诉服务员做好了喊你,然后你可以同时做其他事。在编程中,像用
waitpid
函数时带上WNOHANG
选项,父进程调用后,如果子进程没结束,它不会一直卡在那,而是马上返回去做自己其他的任务,这就是非阻塞调用 。
pid_ t waitpid(pid_t pid, int *status, int options);
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
-
非阻塞调用两种情况
1.在餐厅点餐之后 ,你什么都不干,一直询问服务员好了没。‘’
2.在餐厅等服务员叫你的同时你干些事情。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// 定义函数指针类型
typedef void (*func_t)();
#define NUM 10 // 假设函数指针数组的最大长度
// 以下是任务函数定义
void Download()
{
printf("我是一个下载的任务...\n");
}
void Flush()
{
printf("我是一个刷新的任务...\n");
}
void Log()
{
printf("我是一个记录日志的任务...\n");
}
// 注册函数
void registerHandler(func_t h[], func_t f)
{
int i = 0;
for (; i < NUM; i++)
{
if (h[i] == NULL)
break;
}
if (i == NUM)
return;
h[i] = f;
h[i + 1] = NULL;
}
int main()
{
func_t handlers[NUM] = {NULL};
registerHandler(handlers, Download);
registerHandler(handlers, Flush);
registerHandler(handlers, Log);
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (id == 0)
{
// 子进程
int cnt = 3;
while (1)
{
sleep(3);
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
cnt--;
if (cnt <= 0)
{
break;
}
}
exit(10);
}
// 父进程
while (1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid > 0)
{
if (WIFEXITED(status))
{
printf("wait success, rid: %d, exit code: %d, exit signal: %d\n",
rid, WEXITSTATUS(status), status & 0x7F);
}
else
{
printf("子进程退出异常!\n");
}
break;
}
else if (rid == 0)
{
// 函数指针进行回调处理
int i = 0;
for (; handlers[i]; i++)
{
handlers[i]();
}
printf("本轮调用结束,子进程没有退出\n");
sleep(1);
}
else
{
perror("waitpid");
break;
}
}
return 0;
}
进程替换
fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序
替换来完成这个功能!
程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中!
4.1 替换原理
⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。
4.2 替换函数
其实有六种以exec开头的函数,统称exec函数:
man exec
#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[]);
4.2.1 函数解释
- 这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
- 如果调⽤出错则返回-1
- 所以exec函数只有出错的返回值⽽没有成功的返回值
4.2.1 函数讲解
第一种:
int execl(const char *path, const char *arg, ...);
知识点:对于接下来讲的exec序列函数中的知识点是通用的
1.一旦exec函数替换,就去执行新代码了,原始后面的代码已经不存在了,
2.不做返回值判断,只要返回就是失败了。
引出问题:除了替换指令可以替换自己写的程序吗
先随意写一个语言的代码 c++为例
怎么证明当前你在进行替换的时候,并没有创建新的进程呢,是同一个pid吗?
原理:没有在原替换过程,创建新程序,只是把当前进程的代码和数据用新的程序的代码与数据覆盖式的替换,此时子进程与父进程再也没有关系了,他们分离了。
第二种
execlp("ls","ls","-ln","-a",NULL);
第三种
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("我的程序要运行了!\n");
if(fork() == 0)
{
printf("I am Child, My Pid Is: %d\n", getpid());
sleep(1);
char *const argv[] = { //命令行参数表
(char*const)"ls",
(char*const)"-l",
(char*const)"-a",
NULL
};
execv("/usr/bin/ls", argv);
exit(1);
}
waitpid(-1, NULL, 0);
printf("我的程序运行完毕了\n");
//return 0;
}
小知识:所有进程所需要的命令行参数都是通过父进程用exec 传的命令行参数
第四种带有环境变量
相关问题
1. 关于 execve
更新环境变量后以前环境变量消失
execve
函数执行成功后,会用新程序完全替换当前进程的执行映像,包括代码段、数据段等。新程序启动时,使用的是通过 envp
参数传入的环境变量数组。如果传入的 envp
中不包含之前进程的某些环境变量,那么在新程序中这些之前的环境变量就不存在了。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char *const argv[] = {"echo", "Hello"};
char *const new_envp[] = {
"MY_NEW_VAR=value",
NULL
};
// 执行echo程序,传递新的环境变量数组,原进程环境变量未传入
if (execve("/bin/echo", argv, new_envp) == -1) {
perror("execve");
exit(EXIT_FAILURE);
}
return 0;
}
在上述代码中,execve
执行 echo
程序时,只传入了自定义的环境变量数组 new_envp
,原进程的环境变量没有包含在内,所以 echo
程序运行时看不到原进程的环境变量,就好像之前的环境变量 “消失” 了。
2. 使用 putenv
更新环境变量示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 定义要设置的环境变量字符串
char var_value[] = "MY_VAR=HelloWorld";
// 使用putenv设置环境变量
if (putenv(var_value) != 0) {
perror("putenv");
return EXIT_FAILURE;
}
// 获取并打印环境变量
char *value = getenv("MY_VAR");
if (value != NULL) {
printf("MY_VAR value: %s\n", value);
} else {
printf("Failed to get MY_VAR\n");
}
return EXIT_SUCCESS;
}
在这个例子中,putenv
函数用于将 MY_VAR=HelloWorld
这个环境变量添加到当前进程的环境变量表中。putenv
函数接受一个形如 "变量名=变量值"
的字符串参数,如果设置成功,它不会返回错误值;若失败(比如内存分配问题等 ),则返回非零值。之后通过 getenv
函数获取并打印刚刚设置的环境变量的值。
3. 证明不更新环境变量子进程本身有
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// 子进程
char *value = getenv("PATH");
if (value != NULL) {
printf("子进程中PATH环境变量值: %s\n", value);
} else {
printf("子进程中获取PATH环境变量失败\n");
}
return EXIT_SUCCESS;
} else {
int status;
pid_t wpid = waitpid(pid, &status, 0);
if (wpid == -1) {
perror("waitpid");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
}
在这段代码中,通过 fork
创建子进程。子进程中使用 getenv
函数获取 PATH
环境变量(没有对环境变量进行更新操作 )。由于子进程会继承父进程的环境变量,所以在子进程中可以获取到父进程传递下来的 PATH
环境变量的值,从而证明子进程本身在不更新环境变量的情况下,是有从父进程继承来的环境变量的。
小知识:其实替换函数有七个 ,上面举例了六个,剩下的哪一个是系统调用的,也就是说,为什么那六个就算不写环境变量,也会本身自带,正是因为那六个是语言封装的,自动会调用系统自带的。