目录
一、进程创建
1.1 fork回顾
#include <unistd.h>
pid_t fork(void);功能:从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork(),控制立即从用户空间转移到内核空间,内核执行:
- 创建新的进程控制块(PCB):内核为子进程创建一个新的PCB,用于存储子进程的上下文信息,如寄存器状态、页表、打开的文件描述符等。
- 复制父进程的页表:内核复制父进程的页表,为新进程创建一个相同的虚拟地址空间。父子进程开始共享相同的内存页面。
- 设置新的进程标识:内核为新进程设置一个新的PID,并将其PPID设置为调用fork()的父进程的PID。
- 初始化子进程的状态:内核为新进程设置基本的进程状态,如进程调度信息、信号处理等。
- 复制父进程的环境变量和文件描述符:内核为新进程复制父进程的环境变量和打开的文件描述符。
- 修改子进程的程序计数器:内核将子进程的程序计数器设置为父进程调用fork()之后的下一条指令,这样子进程可以从父进程调用fork()之后的地方开始执行。
- 设置子进程的返回值:对于父进程,fork()返回子进程的PID;对于子进程,返回值为0。
- 更新系统状态:内核更新系统状态,如进程计数、进程队列等。
- 控制返回:内核将控制返回到调用fork()的用户空间程序。
- 子进程开始执行:子进程从调用fork()之后的下一条指令开始执行,而父进程从调用fork()之后的下一条指令继续执行。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
1.2 写时拷贝
写时拷贝(Copy-on-Write,简称COW)是一种内存管理技术,用于提高文件系统的性能和效率。在写时拷贝中,当一个进程试图写入一个共享的内存区域时,系统不会立即复制整个内存区域,而是先保留原来的内存区域,然后在新的内存区域中创建该共享内存的副本。只有在需要写入操作时,才会真正复制内存。
写时拷贝的过程
- 父进程创建子进程时,父进程会将自己页表项的权限由读写改为只读,然后创建子进程,内核会为子进程复制父进程的页表。这样父子进程页表项权限都是只读。
- 这一过程用户不知道,用户可能对某一批数据进行写入。如果子进程尝试修改父进程的物理页面,由于权限设置为只读,操作会被内核拦截,并引发一个异常。(两种情况:①真正出错,例如修改代码区;②不是出错,而是触发重新申请内存拷贝内容的策略机制,需要写时拷贝)
- 此时操作系统介入处理,如果页面是写时拷贝的,内核会为子进程申请新的物理页面进行拷贝,然后更新页表,设置父子进程页表项的权限。
写时拷贝的用途
写时拷贝主要用于文件系统,特别是在支持多进程共享文件的文件系统中。它可以提高文件系统的性能和效率,因为它减少了不必要的内存复制操作。此外,写时拷贝还可以提高系统的安全性,因为它可以防止一个进程的错误影响到其他进程。
二、进程终止
2.1 进程终止时的操作
进程终止后,操作系统需要回收其占用的资源,并通知其父进程。
当一个进程终止时,操作系统会执行以下操作:
- 设置进程状态:操作系统会将进程的状态设置为退出状态。
- 清理资源:操作系统会清理进程占用的资源,如关闭文件描述符、释放内存、终止子进程等。
- 更新进程计数:操作系统会更新进程计数器,以反映进程的终止。
- 通知父进程:操作系统会发送一个信号给进程的父进程,通知它其子进程已经终止。父进程可以通过调用wait()或waitpid()系统调用来接收子进程的终止通知。
2.2 引起进程终止的原因
- 正常结束:当一个进程完成了它的任务或者到达了某个终止点时,它会调用exit()系统调用来通知操作系统它已经完成了。
- 信号处理:进程可以接收到来自其他进程发送的信号,例如SIGKILL(立即终止进程)或SIGTERM(正常终止进程)。如果进程收到这些信号并且没有忽略或处理它们,进程将终止。
- 系统调用:进程可以通过调用exit()、_exit()或return语句来正常终止。这些系统调用会设置进程状态为退出状态,并释放其占用的资源。
- 资源耗尽:如果进程尝试打开太多文件、创建太多进程、使用太多内存等,它可能会因为资源耗尽而被终止。
- 执行非法指令:如果进程尝试执行一个非法指令,如访问未映射的内存地址,它可能会被操作系统终止。
- 系统调用失败:如果进程尝试调用一个系统调用,但系统调用失败(如权限不足),进程可能会被终止。
2.3 退出状态码
在Linux中,C/C++中main函数的返回值通常被解释为程序的退出状态码。如果main函数返回0,通常表示程序成功执行;如果返回非零值,表示程序执行过程中出现了错误。通常某个数字代表某种出错原因。纯数字能表示出错原因,但是不便于人阅读,于是就需要能将数字转换成错误码的字符串描述方案,在C语言中这个方案(接口)就是strerror。
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果。
退出码可以由用户定义:
echo $? 用于打印最近一个命令的退出状态码。
? 保存的是最近一个子进程执行完毕的退出码。$表示取变量的内容
2.4 C语言的错误码 errno
在C语言中,errno是一个全局变量,它被用于存储由系统调用或库函数报错时的出错信息。
例如fork的返回值:创建子进程没有成功时,会适当地设置错误码errno
错误码 VS 退出码
错误码通常是衡量函数或者系统调用时的调用情况。
退出码通常是一个进程退出的时候,它的退出结果。
当失败的时候,二者用来衡量函数、进程出错时的详细出错原因。
很多的系统调用接口在调用失败时都会通过errno表明它的错误,因为Linux内核是用C写的。
错误码和退出码的用法示例:
int main()
17 {
18 int ret = 0;
19 cout << "before: " << errno << endl;
20 FILE *fp = fopen("./log.txt","r");
21 if(fp == nullptr){
22 cout << "after: " << errno << " , errno string: " << strerror(errno) << endl;
23 ret = errno;
24 }
25
26 return ret;
27 }
2.5 进程的退出方式
进程常见退出方式:
- 正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit
3. _exit- 异常退出(进程退出码无意义)
1. ctrl + c
2. 信号终止
操作系统是进程的管理者,进程异常时操作系统要处理这个异常情况。进程出异常时,它的异常信息会被操作系统检测到,并被转换成信号,然后终止进程。进程出异常,本质是进程收到了对应的信号,自己终止了。
所以,一个进程是否出异常,我们只要看有没有收到信号即可。如果没有异常,就根据进程的退出码来判断 。子进程终止,父进程可以获取子进程的退出状态码和信号信息。
#include <stdlib.h>
void exit(int status);功能:终止当前进程并返回给调用它的进程一个退出状态码。类似于main函数的return n
参数:进程的退出状态码。这个值可以被传递给父进程,父进程可以通过调用wait()或waitpid()系统调用来获取子进程的退出状态。
注:在代码中的任意地点调用exit,表示进程退出,不再进行执行后续代码。
#include <unistd.h>
void _exit(int status);功能:系统调用,用于立即终止当前进程,并返回给父进程一个整数退出状态码。
参数:status 定义了进程的终止状态,父进程通过wait来获取该值注:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
_exit 和 exit 的区别:
- exit 是库函数,_exit 是系统调用。
- exit终止进程的时候,会自动刷新缓冲区。_exit终止进程的时候,不会自动刷新缓冲区。
之前讲到在C语言中,有一种刷新输出缓冲区的方式为程序退出时刷新:当程序正常终止时,输出缓冲区通常会被自动刷新。但是,如果程序异常终止(如调用_exit()函数)或者使用非正常的终止方式,输出缓冲区可能不会被刷新。
注:
- 缓冲区不在操作系统内部。exit是库函数,使用时会调用_exit这个系统调用, 如果缓冲区在操作系统内部,那么_exit也会顺便把缓冲区刷新。那么缓冲区就可能在C标准库中。之后会进行解释。
三、进程等待
3.1 进程等待的概念
通过调用wait()或waitpid(),让父进程(一般)对子进程进行资源回收的等待过程。
3.2 进程等待的必要性
- 解决'僵尸进程'造成的内存泄漏问题。(僵尸进程是指已经结束执行但仍然在进程表中占据一个位置的进程。)
- 创建子进程是为了完成某项任务,父进程要知道派给子进程的任务完成的如何。
例如子进程运行完成结果对还是不对,或者是否正常退出。 - 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。(两个数字:退出状态码、信号编号)
3.3 进程等待的方法
3.3.1 wait 方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);功能:
系统调用,用于父进程等待其任意一子进程的终止。当父进程调用wait()时,它会暂停执行,直到至少有一个子进程终止。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL返回值:
成功返回被等待进程pid,失败返回-1。注:
- 如果子进程没有退出,父进程必须在wait上进行阻塞等待。直到子进程僵尸,然后wait自动回收,返回!
- 进程等待条件没有就绪时,进程处于阻塞状态(S),一个进程不仅能等待硬件资源是否就绪(键盘是否输入),也可以等待软件资源是否就绪(例如子进程是否退出就绪),如果软硬件条件不就绪,进程将进入阻塞或挂起状态。
- 所以父进程在wait时就在阻塞等待。一般而言父进程最后退出,多进程执行流由父进程发起,也由父进程回收。
3.3.2 waitpid方法
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
功能:
系统调用,用于父进程等待指定进程ID(pid)的子进程的终止。这个系统调用提供了比wait()更多的灵活性,因为它允许父进程指定要等待的子进程ID,并且可以设置非阻塞等待选项。
参数:
- pid:指定要等待的子进程的PID。
pid = -1 等待任一个子进程。与wait等效。
pid > 0 等待指定子进程。- status:指向一个整数变量的指针,用于存储子进程的退出状态码。(输出型参数)
WIFEXITED(status): 这是一个宏,用于对status中的信号值做检测,若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)- options: 是一个掩码,用于指定等待子进程时的行为。
默认设置为0:表示以阻塞方式等待。
WNOHANG: 表示等待时,以非阻塞方式进行等待。返回值:(当options设置为WNOHANG时会有3种返回值,若options为0,返回值同wait)
- > 0 :正常返回,waitpid返回收集到的子进程的PID;
- = 0 :等待成功,但等待的进程还没有退出。
- < 0 :等待失败,例如调用进程出错。
注:
- status是一个输出型参数,需要用户自己定义一个整型变量,操作系统将status对应的值赋给该变量。
- status对应一个int整数,但它不是整存整取的,它的32个bit被按照不同的区域划分,表示不同的含义。我们在使用时只用考虑它的低16位。
正常退出时,次低8位为退出码,低8位为0。
异常退出时,次低8位(退出码)没有意义,低7位为终止信号。- 如果子进程exit(10)退出,即正常退出,status返回的值应该为2560
原因:10的二进制为1010,那么status的次低8位为0000 1010,低16为0000 1010 0000 0000 翻译成十进制正好为2560。- 我们不能对status整体使用,因为不知道是否为正常退出。
- kill -l 查询数字对应的信号(信号列表是没有0号信号的)
- 退出信号为0,退出码为0,表示正常退出,结果正确。
退出信号为0,退出码非0,表示正常退出,结果不正确。
退出信号非0,表示异常退出。可通过信号知道原因。
指针指向的是0号地址,对0号地址写入会触发非法写入。
3.3.3 waitpid 更详细的学习
父进程如何得知子进程的退出信息?
父进程可以通过调用wait()或waitpid()系统调用来得知子进程的退出信息。wait()和waitpid()会设置参数int* statusp指向status,子进程退出时会将状态设置为'Z',并且在PCB中写入退出码、退出信号。wait将退出码、退出信号写入status中
(例如 *statusp = (exit_code<<8) | exit_signal)
原因:操作系统不允许用户直接访问PCB,所以设置了wait/waitpid来获取PCB的内容。
回顾阻塞和等待:
进程可能会访问底层的某些设备,例如磁盘和显示器,而每一个设备被操作系统识别和管理,就需要先描述再组织,所以每个设备在内核中都有它对应的结构体。一个进程在一个设备上等待,本质是设备给它提供了等待队列。所以一个进程的状态由'R'改成'S'时,把PCB链入到设备的结构体对象,那么当前进程就在设备里等待。同理,父进程在等待子进程时,也是将自己的PCB链入到子进程的等待队列中。当子进程退出时,操作系统直接将父进程从子进程的等待队列中取出,然后调度父进程。即执行了父进程对应的wait。
注:
- 不仅设备有等待队列,每个PCB也有自己的等待队列。
- 一个进程在等待时阻塞,那么一定在等待队列中等待。
- 在等待某个进程资源就绪时,可以把等待进程放在被等待进程中。
使用WIFEXITED和WEXITSTATUS宏
WIFEXITED(status): 这是一个宏,用于对status中的信号值做检测,若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
父进程等待多个子进程时,使用waitpid最好不要按照创建子进程的顺序等待子进程(例如创建一个子进程pid表),因为如果子进程没有结束,父进程就会阻塞,即使后创建的子进程已经结束也无法继续,可使用-1(代表等待任意一个子进程)。
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
void Worker(int number)
{
//int* p = NULL;
for(int i = 0; i < 3; i++)
{
cout << "I am a child process, pid: " << getpid() << ", ppid: " << getppid() << ", number: " < < number << ", i: " <<i << endl;
sleep(1);
//*p = 100;
}
//exit(10);
}
const int n =10;
int main()
{
for(int i = 0;i < n; i++)
{
pid_t id = fork();
if(id == 0)
{
Worker(i);
exit(i);
}
}
//等待多个子进程
for(int i = 0; i < n; i++)
{
int status = 0;
pid_t rid = waitpid(-1,NULL,0);
if(rid > 0){
cout << "wait child " << rid << " success, exit_code: " << WEXITSTATUS(status) << endl;
}
}
return 0;
}
我们为什么不用全局变量获取子进程的退出信息?而是用系统调用?
原因:定义的全局变量在父进程中已经存在,如果子进程退出时将退出状态写入该全局变量,则会触发写时拷贝,此时父进程是读不到这个值的。
进程具有独立性,父进程无法读到子进程的退出信息。
引入两个概念
宕机(Downtime): 宕机是指系统或服务无法正常工作的一段时间。例如蓝屏。这可能是由于硬件故障、软件错误、网络问题、电源中断、维护或升级等原因造成的。
夯(Thrashing): 夯是指在存储器(尤其是磁盘存储)中频繁地读写数据,导致系统性能急剧下降的现象。例如进程很多,电脑特别卡。这种情况通常发生在系统内存不足时,操作系统不断地在内存和磁盘之间交换页面,以尝试满足所有运行的进程的需求。这种频繁的页面交换会导致CPU利用率很高,但实际完成的工作却非常少,因为大部分时间都花在了读写操作上,而不是处理数据上。
waitpid的第三个参数:options (是一个掩码,用于指定等待子进程时的行为。)
- 默认设置为0:表示以阻塞方式等待。
- WNOHANG:表示等待时,以非阻塞方式进行等待。
若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
若正常结束,则返回该子进程的ID。阻塞等待 VS 非阻塞等待
阻塞等待:子进程不退出,wait不返回,此时父进程什么也干不了。
非阻塞等待:进行等待的过程中,可以做一些占据时间不多的事情。
WNOHONG 的示例:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t child_pid, terminated_pid;
// 创建一个子进程
child_pid = fork();
if (child_pid == 0) {
// 子进程
printf("Child process is running\n");
sleep(5); // 子进程睡眠5秒
printf("Child process is exiting\n");
_exit(0); // 子进程退出
} else if (child_pid > 0) {
// 父进程
int status;
do {
terminated_pid = waitpid(child_pid, &status, WNOHANG);
if (terminated_pid == 0) {
printf("Child is still running\n");
sleep(1); // 等待1秒
}
} while (terminated_pid == 0);
if (terminated_pid == child_pid) {
printf("Child has terminated\n");
} else {
printf("waitpid error\n");
}
} else {
// 出错
printf("fork error\n");
}
return 0;
}
父进程使用 WNOHANG 选项定期检查子进程是否已经结束,并在子进程结束前执行其他任务(在这个例子中是打印一条消息并等待1秒)。一旦子进程结束,waitpid 将返回子进程的ID,父进程可以继续执行后续操作。