一、虚拟地址空间
我们在学C语言的时候,会把程序的代码和数据存储理解成下面这张图:

当时是怎么验证的呢?我们看了编译的代码,发现编译的后的汇编里每条指令都有地址,所以就是这样存的。但是,为什么要这样做呢?每条指令的地址在编译时就定死了不是很奇怪吗?
其实,我们看到的地址是虚拟地址。是操作系统想让你看到的地址,而真实的地址(物理地址)是被操作系统管理起来的,一般用户见不到。
操作系统不仅负责管理物理地址,还要做到物理地址和虚拟地址之间的相互映射。
之前说的程序地址空间是不准确的,应该叫做进程地址空间,为什么呢?

在一个程序中,若存在创建进程,则父子进程会各自拥有一块地址空间。这导致一个程序有多个地址空间,而一个进程只有一个地址空间,因此应该叫做进程地址空间。
而虚拟地址和物理地址的映射,是由页表来实现的。
在进程task_struct中,有一个mm_struct的结构体,被称为内存描述符,用于描述进程的虚拟内存,但是这样就可以做到相互之间互不干扰。

面对这么多的虚拟区间,操作系统自然需要维护起来,所以,对于虚拟空间,操作系统的维护方式有以下两种:
1.当虚拟区间少时采用单链表,mmap指针指向链表。
2.当虚拟区间多时采用红黑树进行管理,mmrb指向这棵树
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进程快速访问。
更细致的图如下:

那为什么操作系统要采用虚拟地址空间呢?
其实有很多原因,比如为了安全考虑,防止外部直接访问内存进行破坏。比如为了解耦合,为了让用户屏蔽底层逻辑。
二、进程创建
我们一般使用系统调用创建程序,而在Linux中的进程调用使用的是fork
#include<unistd.h>
pid_t fork(void)
其中,返回值pid_t其实就是整型。返回值有以下几种情况:
1.出错,返回-1。
2.作为父进程,返回子进程id。
3.作为子进程,返回0。
当系统调用fork时:
1.内核将父进程的内存块和内核数据结构给子进程。
2.将父进程的部分数据结构内容拷贝到子进程中。
3.将子进程添加到进程列表中。
4.fork返回,调度器开始调度。
写时拷贝
父子的代码是共享的,而数据默认是只读,也是共享的,当一方尝试对某一个数据进行写入,此时会先将其拷贝一份给子进程,再进行写入,这样要用时才写入的过程被称为写时拷贝,写时拷贝可以提高内存的利用率。
常见fork失败的原因
1.系统中有太多的进程。
2.实际用户进程数超过了限制。
三、进程终止
进程终止本质上是释放资源,也就是释放进程申请的内核数据结构和相关的代码和数据。
进程退出有三种情况:
- 进程正常退出,结果正确。
- 进程正常退出,结果不正确。
- 进程异常终止。
为了描述进程的退出情况,就有了退出码。
命令echo $?可以查看最近进程(正常退出)的退出码。
正常退出的方式:
- 1.return
- 2.调用exit或_exit
异常退出:
- ctrl+c 进程终止
Linux中进程的主要退出码基本相同:

#include<unistd.h>
void _exit(int status);
void exit(int status);
status代表的就是退出码
而与_exit相比,exit做了这些工作:
- 执行用户通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
return n其实相当于exit(n)。
四、进程等待
进程等待的重要性
为了防止僵尸进程造成内存泄漏,我们必须回收进程资源和进程退出信息。而进程等待是由父进程进行的。
进程等待的方法
wait函数:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
返回值为等待的子进程的pid,等待失败则返回-1。
参数是输出型参数,若不关心可以设为null。
waitpid函数:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
返回值为等待的子进程的pid,等待失败则返回-1,若设置选项WHOHANG且没有等到子进程,则返回0。
pid:若设置为0,则等待任意一个进程,若大于零则等待进程id与参数相等的子进程。
wstatus:输出型参数
WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
的退出码)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:默认为0,表示阻塞等待。
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该子进程的ID。
status参数详解
status是由操作系统填充的输出型参数,用于查看子进程的退出信息。
status并不是简单的整型,而是位图结构,具体形式如下图:

阻塞与非阻塞等待
//阻塞等待
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Failed to fork!\n";
return 1;
} else if (pid == 0) {
// 子进程代码块
std::cout << "Child process (PID: " << getpid() << ") started.\n";
sleep(2);
std::cout << "Child process exiting.\n";
// 子进程退出,返回状态码20
return 20;
} else {
// 父进程代码块
std::cout << "Parent process (PID: " << getpid() << ") waiting for child...\n";
int status; // 用于存储子进程的退出状态
pid_t child_pid = waitpid(pid, &status, 0);
// 检查 waitpid 是否成功
if (child_pid == -1) {
std::cerr << "waitpid failed!\n";
return 1;
}
// 检查子进程是否正常退出
if (WIFEXITED(status)) {
// WIFEXITED(status)
// WEXITSTATUS(status)码
std::cout << "Child process (PID: " << child_pid
<< ") exited with status: " << WEXITSTATUS(status) << "\n";
} else if (WIFSIGNALED(status)) {
std::cout << "Child process (PID: " << child_pid
<< ") killed by signal: " << WTERMSIG(status) << "\n";
}
std::cout << "Parent process continuing...\n";
}
return 0;
}
//非阻塞等待
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unistd.h>
int main() {
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Failed to fork!\n";
return 1;
} else if (pid == 0) {
// 子进程代码块
std::cout << "Child process (PID: " << getpid() << ") started.\n";
std::cout << "Child process working...\n";
sleep(3);
std::cout << "Child process exiting.\n";
// 子进程退出,返回状态码20
return 20;
} else {
// 父进程代码块
std::cout << "Parent process (PID: " << getpid() << ") created child with PID: " << pid << "\n";
int status; // 用于存储子进程的退出状态
int wait_count = 0; // 记录等待次数
// 非阻塞等待循环
while (true) {
pid_t child_pid = waitpid(pid, &status, WNOHANG);
if (child_pid == -1) {
// waitpid 出错
std::cerr << "waitpid failed!\n";
return 1;
} else if (child_pid == 0) {
// 子进程尚未退出,waitpid 立即返回
wait_count++;
std::cout << "Child process not finished yet. Parent doing other work... (check #"
<< wait_count << ")\n";
sleep(1);
} else {
// 子进程已退出
std::cout << "Child process (PID: " << child_pid << ") has finished.\n";
// 检查子进程退出状态
if (WIFEXITED(status)) {
std::cout << "Child exited normally with status: " << WEXITSTATUS(status) << "\n";
} else if (WIFSIGNALED(status)) {
std::cout << "Child was terminated by signal: " << WTERMSIG(status) << "\n";
} else if (WIFSTOPPED(status)) {
std::cout << "Child was stopped by signal: " << WSTOPSIG(status) << "\n";
} else if (WIFCONTINUED(status)) {
std::cout << "Child was resumed by SIGCONT\n";
}
break;
}
}
std::cout << "Parent process continuing after " << wait_count << " checks.\n";
}
return 0;
}
五、进程程序替换
子进程创建后,父子进程各自执行父进程代码的一部分,有没有什么办法让子进程执行一个全新的程序呢?有的,这种办法被称为程序替换。
程序替换是通过某种接口,从磁盘上加载一个全新的程序,加载到调用程序替换的进程地址空间中。
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种 exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec并不创建新进程,所以调用 exec前后该进程的id并未改变。

替换函数
替换函数一共有六种,都以exec开头,它们被统称为exec函数。
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[]*/);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
这些函数如果调用成功直接执行新的代码,不返回。
如果调用失败则返回-1。
exec函数命名
exec的命名都是有意义的,具体含义如下:
l(list):表示是否参数采用列表
v(vector):参数采用数组
p(path):自动搜索环境变量PATH
e(env):自己维护环境变量

事实上,只有execve是系统调用,exec函数之间的关系如下图:

1059

被折叠的 条评论
为什么被折叠?



