🌇 前言 进程 创建后,需要对其进行合理管理,光靠 OS 是无法满足我们的需求的,此时可以运用 进程 控制相关知识,对 进程 进行手动管理,如创建 进程、终止 进制、等待 进程 等,其中等待 进程 可以有效解决僵尸 进程 问题。
1、进程创建
在学习 进程控制 相关知识前,先要对回顾如何创建 进程,涉及一个重要的函数 fork
1.1、fork函数
#include <unistd.h> //所需头文件
pid_t fork(void); //fork 函数
fork 函数的作用是在当前 进程 下,创建一个 子进程,子进程 创建后,会为其分配新的内存块和内核数据结构(PCB),将 父进程 中的数据结构内容拷贝给 子进程,同时还会继承 父进程 中的环境变量表
进程具有独立性,即使是父子进程,也是两个完全不同的进程,拥有各自的 PCB
假设 子进程 发生改写行为,会触发写时拷贝机制
fork 函数返回类型为 pid_t,相当于 typedef int,不过是专门用于进程的,同时它拥有两个返回值:
- 如果进程创建失败,返回 -1
- 进程创建成功后
- 给子进程返回 0
- 给父进程返回子进程的 PID 值。
- 进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main( void )
{
pid_t id;
printf("Before: pid is %d\n", getpid());
// id = fork 先创建进程并赋值,返回值为-1则创建失败,直接退出
if ( (id=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), id);
sleep(1);
return 0;
}
1.2、写时拷贝
在【进程地址空间】一文中,谈到了写时拷贝机制,实现原理就是通过 页表+MMU
机制,对不同的进程进行空间寻址,达到出现改写行为时,父子进程使用不同真实空间的效果
验证写时拷贝现象很简单,创建子进程后,使其对生命周期长的变量作出修改,再观察父子进程的结果即可
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件
const char* ps = "This is an Apple"; //全局属性
int main()
{
pid_t id = fork();
if(id == 0)
{
ps = "This is a Banana"; //改写
printf("我是子进程,我认为:%s\n", ps);
exit(0); //子进程退出
}
wait(0); //等待子进程退出
printf("我是父进程,我认为:%s\n", ps);
return 0;
}
不难发现,子进程对指针 ps 指向内容做出改变时,父进程并不受影响,这就是写时拷贝机制
通过地址打印,发现父子进程中的 ps 地址一致,因为此时是虚拟地址
在虚拟地址相同的情况下,真实地址是不同的,得益于 页表+MMU 机制寻址不同的空间
写时拷贝机制本质上是一种按需申请资源的策略。
注意:
- 写时拷贝不止可以发生在常规栈区、堆区,还能发生在只读的数据段和数据段
- 写时拷贝后,生成的是副本,不会对原数据造成影响
2、进程终止
进程终止是在做什么?
1、释放曾经的代码和数据所占据的空间。
2、释放内核数据结构。
3.进程退出和错误处理
1. 退出码(Exit Status)
退出码是进程终止时返回给操作系统的状态码。在类 Unix 操作系统中,退出码通常是一个整数,表示程序的执行结果。程序通过 exit()
函数或 return
语句返回退出码。
-
0:表示程序正常结束,成功执行。
-
非零值:表示程序发生错误或异常,通常 1 表示一般错误,其他非零值表示不同类型的错误。
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Program started.\n");
exit(0); // 正常退出,返回退出码 0
}
2.退出码和 echo $?
在 Shell 中,echo $?
用于显示上一个执行命令的退出状态。例如,ls
命令执行成功时,退出码为 0;如果 ls
执行失败,退出码则为非零。
注意:当进程因为接收到信号而终止时,退出码(Exit Status)通常会包含该信号的编码,而不是程序的返回值。信号编码通常对应于信号的数字编号,比如 SIGSEGV
(段错误)信号的编号通常是 11。
3. 退出信号(Exit Signal)
退出信号是指进程因收到某些信号(如 SIGKILL
、SIGSEGV
等)而终止时的状态。当进程因信号而终止时,它的退出状态(退出码)会被设置为该信号的编号,通常通过 WTERMSIG(status)
获取。
-
例如,子进程如果由于接收到
SIGSEGV
(段错误)信号而终止,父进程通过waitpid()
获取的状态中会包含该信号的编号。#include<stdio.h> #include<sys/types.h> #include<unistd.h> int main() { int* p = NULL; printf("I am a process,pid:%d,ppid:%d\n",getpid(),getppid()); *p = 100;// 对空指针进行解引用 return 0; }
kill -l 可以查看信号对应的错误,但是echo $? 查到的139,并没有找到,是因为实际的的序号是139-128=11。11才是真正的序号。
4. errno
和 strerror()
errno
是一个全局变量,用于记录系统调用或库函数失败时的错误码。每当系统调用或标准库函数失败时,errno
会被设置为相应的错误码。例如,当你尝试打开一个不存在的文件时,fopen()
会返回 NULL
,并将 errno
设置为 ENOENT
(表示文件不存在)。
-
errno
:保存系统调用失败的错误码(例如文件打开失败、内存分配失败等)。 -
strerror()
:是一个函数,它接受errno
的值并返回一个指向描述该错误的字符串的指针。你可以通过strerror(errno)
来打印出系统调用失败的原因。#include <stdio.h> #include <errno.h> #include <string.h> int main() { FILE *file = fopen("nonexistent_file.txt", "r"); if (file == NULL) { printf("Error: %s\n", strerror(errno)); // 打印错误信息 } return 0; }
5. exit()和_exit()
exit()
是 C 标准库函数,用于终止程序并返回一个退出状态码。程序通过 exit()
来向操作系统报告它的退出状态。exit()
可以传递一个整数作为参数,这个整数就是退出码。
-
exit(0)
表示程序成功退出。 -
exit(1)
或其他非零值表示程序异常退出。
exit()
和 _exit()
的区别:
-
exit()
:-
会做清理工作:例如调用
atexit()
注册的清理函数,刷新输出缓冲区,关闭打开的文件等。 -
用于正常退出程序,确保资源被正确释放。
-
-
_exit()
:-
不做清理工作:不会调用
atexit()
函数,不会刷新输出缓冲区,也不会关闭文件描述符。 -
通常用于子进程退出时,或者需要立即退出时(比如
fork()
后的子进程退出)。
-
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
- 1. 执行用户通过 atexit或on_exit定义的清理函数。
- 2. 关闭所有打开的流,所有的缓存数据均被写入。
- 3. 调用_exit。
当你在 C 程序中调用
exit(-1)
,并且在 Shell 中通过echo $?
查到退出码为 255,这是因为:解释:
在 Unix 和 Linux 系统中,退出码(Exit Status)是一个 8 位的无符号整数,即其范围是 0 到 255。因此,程序的退出码必须是一个在这个范围内的值。
exit()
函数允许传递一个整数值作为退出码。虽然exit()
的参数可以是任何整数,但操作系统对这个值有一定的限制。
如果你传递负数(如
-1
)给exit()
,操作系统会自动将其转换为 8 位无符号整数。如何转换:负数
-1
会被转化为一个无符号整数,它的表现形式是255
。计算方式:
-1
在补码表示法中是11111111
(即 255 的二进制表示)。因此,当你调用
exit(-1)
时,操作系统将其视为退出码255
。
6. 它们之间的关系
-
退出码与
exit()
:程序通过exit()
或return
来传递退出码,表示程序的终止状态。父进程或调用进程可以通过waitpid()
或wait()
获取子进程的退出码。 -
退出码与
echo $?
:Shell 使用echo $?
显示上一个命令的退出状态码(退出码表示命令的成功与否)。例如,ls
命令成功执行时,退出码为 0;如果命令失败,退出码为非零值。 -
退出信号与
exit()
的退出码:如果进程因信号(如SIGKILL
、SIGSEGV
等)而终止,退出状态是由信号编号表示的。此时退出码(退出状态)将会被设置为信号的编号。 -
errno
和退出码的区别:errno
用于表示系统调用的错误(如文件操作失败、内存分配失败等)。它不与进程退出状态直接相关。exit()
的退出码是程序本身设定的,而errno
是在系统调用或库函数失败时由操作系统设置的。 -
strerror()
和errno
:strerror()
用来根据errno
错误码返回相应的错误信息描述。它与程序退出码无关,而是用于获取和显示系统调用失败的具体错误原因。