一、内存相关知识点(分页机制 + 程序地址空间)
(一)内存分页核心概念(虚拟内存与物理内存映射)
1. 页的核心定义与分类
| 概念 | 别称 | 含义说明 | 关键特性 |
|---|---|---|---|
| 页面 | 虚页面 | 虚拟地址空间中的最小单位,进程逻辑地址空间被划分为若干个页面 | 大小固定(常见 4K、8K),属于虚拟内存范畴 |
| 页框 | 物理页面 | 物理内存中的最小单位,物理内存被划分为与页面大小相同的若干个页框 | 与页面大小一一对应,属于实际物理内存范畴 |
| 页表 | - | 核心数据结构,用于记录 "页面(虚拟)" 与 "页框(物理)" 的映射关系 | 是操作系统实现虚拟内存到物理内存映射的关键 |
2**. 页的核心作用**
- 实现虚拟内存与物理内存的映射:让程序通过逻辑地址(页面)间接访问物理地址(页框),无需直接操作硬件物理内存
- 简化内存管理与分配:操作系统以页为单位分配内存,无需管理零散的字节级内存,降低管理复杂度
- 提高内存利用率与系统性能:通过内存共享、按需分配等机制,减少内存浪费,提升系统整体运行效率
3. fork () 的写时拷贝(Copy-On-Write,COW)机制
- 核心原理:fork () 创建子进程时,不会立即拷贝父进程的所有页面,而是让父子进程共享所有页面(包括代码段、数据段等);以 "页" 为单位进行延迟拷贝,只有当父进程或子进程修改某一页中的任意一个字节时,操作系统才会拷贝该页面(为修改方创建独立副本);若页面始终未被修改,则父子进程永久共享该页面,无需拷贝
- 优势:提高 fork () 效率,避免不必要的内存拷贝;节省内存资源,未修改的页面无需占用额外物理内存
(二)程序内存分区(程序地址空间)
核心分区详情(按地址从低到高)
| 分区名称 | 存储内容 | 关键特性 |
|---|---|---|
| 代码段 | 程序的可执行指令(机器码)、只读常量(如字符串常量 "hello") | 内存只读(防止指令被意外修改),编译时大小固定 |
| 数据段 | 已初始化的全局变量、已初始化的静态变量(包括 static 修饰的局部变量) | 编译时分配内存,程序运行期间一直存在(直到程序结束),内存可读写 |
| 堆区 | 程序运行期间动态申请的内存数据(如 malloc、new 创建的变量 / 对象) | 手动分配与释放(需用 free/delete 回收,否则导致内存泄漏),内存大小不固定,可动态增长,分配效率低于栈区 |
| 栈区 | 函数的局部变量、函数参数、函数调用时的返回地址 | 自动分配与释放(函数调用时分配,函数返回时自动回收),遵循 "先进后出" 原则,内存大小固定(溢出会导致栈溢出错误) |
二、进程相关知识点(僵死进程 + 孤儿进程 + wait 函数 + 系统调用与库函数区别)
1. 僵死进程(僵尸进程)
- 核心定义:子进程先于父进程结束,父进程未及时获取子进程的退出码(未回收子进程资源),子进程的 PCB(进程控制块)无法被销毁,残留于系统中,这类子进程称为僵死进程
- 关键特性:
- 退出码(return/exit 的返回值)会存储在子进程的 PCB 中
- 危害:占用系统 PID 资源和内存空间,大量僵死进程会导致无法创建新进程
- 产生主要原因:父进程未调用 wait ()/waitpid () 函数回收子进程退出码
2. 孤儿进程
- 核心定义:父进程先于子进程结束,子进程尚未结束,这类子进程称为孤儿进程
- 关键特性:
- 孤儿进程会被系统分配 "养父" 进程:老内核版本中是 init 进程(PID=1),现代内核版本中不一定是 PID=1 的进程
- 养父进程会负责回收孤儿进程的退出码和资源,避免孤儿进程成为僵死进程
3. wait () 函数(子进程资源回收)
(1)基础信息
- 头文件:
#include <sys/wait.h> - 核心功能:专门用于父进程接收子进程的退出码,回收子进程资源,防止子进程变为僵死进程
- 返回值:成功返回子进程的 PID,失败返回 - 1(如无待回收的子进程)
(2)关键特性
- 阻塞特性:父进程调用 wait () 后,会进入阻塞状态,暂停执行 wait () 之后的代码,直到等待到子进程结束并获取其退出码,才会继续执行后续代码
- 执行顺序:子进程先运行并结束,父进程获取退出码完成 wait () 操作后,再继续运行
(3)退出码相关判断函数(配套 wait () 使用)
| 函数 | 功能说明 | 参数 |
|---|---|---|
WIFEXITED(status) |
判断子进程是否正常退出(通过 return/exit 退出,非异常终止) | wait () 返回的状态值 |
WEXITSTATUS(status) |
若 WIFEXITED 返回真,自动获取子进程的退出码(return/exit 的返回值) | wait () 返回的状态值 |
cpp
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程
int status;
pid_t child_pid = wait(&status); // 阻塞等待子进程结束
if (WIFEXITED(status)) {
printf("子进程PID=%d 正常退出,退出码=%d\n", child_pid, WEXITSTATUS(status));
}
printf("父进程继续执行后续代码\n");
} else if (pid == 0) {
// 子进程
printf("子进程运行中,即将退出\n");
exit(10); // 子进程正常退出,退出码=10
} else {
perror("fork失败");
return 1;
}
return 0;
}
4. 系统调用与库函数的核心区别
(1)本质差异
| 类型 | 执行流程 | 示例 |
|---|---|---|
| 系统调用 | 程序会产生中断,从用户态切换到内核态,通过系统调用号(存于 eax 寄存器)匹配系统调用表,执行内核函数后,再从内核态切换回用户态并返回结果 | fork()、open()、read()、write()、close() |
| 库函数 | 运行在用户态,无需切换到内核态(部分库函数底层会调用系统调用,如 printf () 底层调用 write ()) | printf()、fopen()、strlen()、malloc() |
(2)内核态的意义
相当于 "专业的事交给专业的人做"(类比银行存钱取钱:用户无需直接接触金库,由银行工作人员(内核)处理核心操作),保障系统安全和资源管理的规范性。
(3)特殊说明
- printf () 虽为库函数,但底层依赖系统调用 write () 实现输出功能
- 每个系统调用都有唯一的系统调用号(如 1=sys_fork ()、2=sys_exit ()),存储在系统调用表中
三、进程替换(exec 系列函数)
1. 核心定义
进程替换是指将当前进程的内存空间(代码、数据、堆栈等)全部销毁,替换为新的程序,新程序从头开始运行,总的进程数不变,原进程的 PID 被新程序继承。
2. 常见 exec 系列函数
- 列表型:
execl()、execlp()、execle() - 矢量型:
execv()、execvp()
3. 关键特性
- 替换后原进程的所有资源(内存数据)全部消失,新程序完全替代原进程
- exec 函数执行成功后,不会返回原进程(新程序直接运行);只有执行失败时,才会返回 - 1 并继续执行原进程后续代码
- Linux 中 exec () 参数含义:1 = 可执行程序 /shell 命令,2 = 系统调用,3 = 库函数
- 进程替换不改变进程 PID,仅替换进程内部的程序逻辑和内存数据
4. 使用示例(execlp 执行 ls 命令)
cpp
#include <unistd.h>
#include <stdio.h>
int main() {
printf("原进程PID=%d,即将替换为ls命令\n", getpid());
// 替换为ls命令,列出当前目录文件(参数以NULL结尾)
int ret = execlp("ls", "ls", "-l", NULL);
// 若exec执行成功,下面的代码不会执行
if (ret == -1) {
perror("exec失败");
return 1;
}
printf("原进程后续代码(不会执行)\n");
return 0;
}
5**. 进程替换与 fork () 的配合使用场景**
- 常见用法:父进程通过 fork () 创建子进程,子进程通过 exec () 进行进程替换,父进程通过 wait () 回收子进程资源
- 优势:既保留了原进程(父进程),又能通过子进程执行新程序,不影响父进程的正常运行
配合使用示例
cpp
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程:等待子进程执行完毕
int status;
wait(&status);
printf("子进程执行完成,父进程继续运行\n");
} else if (pid == 0) {
// 子进程:替换为echo命令
printf("子进程PID=%d,执行echo命令\n", getpid());
execlp("echo", "echo", "Hello 进程替换!", NULL);
// 替换失败才会执行以下代码
perror("exec替换失败");
exit(1);
} else {
perror("fork创建子进程失败");
return 1;
}
return 0;
}
四、文件操作(Linux 系统调用)
1. 基础认知
- Linux 核心思想:一切皆文件(普通文件、设备、管道等均以文件形式管理)
- 系统调用文件操作核心函数:
open()、close()、read()、write() - 与 C 语言文件操作(
fopen()/fread()/fwrite())本质功能一致,仅接口形式不同,无需额外学习新逻辑
2. open () 函数(打开 / 创建文件)
(1)基础信息
- 头文件:
#include <fcntl.h> - 函数原型:
int open(const char* pathname, int flags, mode_t mode); - 返回值:成功返回文件描述符(进程文件表的下标),失败返回 - 1
(2)参数详细说明
| 参数名 | 功能说明 | 常用取值 / 示例 | |
|---|---|---|---|
| pathname | 要打开 / 创建文件的路径名,支持绝对路径和相对路径 | "./a.txt"(当前目录 a.txt)、"/home/stu/test.txt"(绝对路径) | |
| flags | 打开文件的方式与标志,可组合使用(用 ` | ` 连接) | 1. 访问模式:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)2. 附加标志:O_CREAT(文件不存在则创建)、O_TRUNC(文件存在则清空内容) |
| mode | 文件权限(仅当 flags 包含 O_CREAT 时有效),支持两种指定方式 | 1. 数字形式:0644(所有者读写、同组读、其他人读)、0755(所有者读写执行、同组读执行、其他人读执行)2. 宏定义形式:S_IRUSR(用户读)、S_IWUSR(用户写)、S_IXUSR(用户执行) |
(3)使用示例(创建并打开 a.txt)
cpp
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建并打开a.txt,只写权限,文件权限0644
int fd = open("a.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open失败");
return 1;
}
printf("文件打开成功,文件描述符=%d\n", fd);
close(fd); // 关闭文件
return 0;
}
3**. 文件描述符与文件表**
(1)文件描述符(fd)
-
本质:非负整数,是进程文件表的下标,用于唯一标识进程当前打开的文件
-
默认分配的文件描述符(进程启动时自动初始化,占用文件表前 3 个位置):
文件描述符 对应设备 / 文件 标准标识 用途 0 键盘 stdin 标准输入 1 屏幕 stdout 标准输出 2 屏幕 stderr 标准错误输出 -
用户手动打开的文件,从文件描述符 3 开始依次分配
(2)文件表核心特性
- 每个进程对应一个独立的文件表,存储该进程所有已打开文件的描述符与关联信息
- 空闲描述符复用:关闭某个文件后,其对应的文件描述符会变为空闲,后续打开新文件时,会优先使用该空闲描述符(不会让描述符连续递增,也不会移动已打开文件的描述符位置)
- 资源占用风险:不关闭无用文件会持续占用文件描述符和文件表资源,最终导致无法打开新文件
4. struct file 结构体
- 每个打开的文件对应一个
struct file结构体,存储文件的核心元信息:文件权限、文件偏移量、文件类型、文件对应的物理存储位置等 - 生命周期:文件打开时创建,文件关闭时销毁,同时释放对应的文件描述符
5. read () 函数(读取文件内容)
(1)核心特性
- 函数功能:从指定文件描述符对应的文件中读取数据到缓冲区
- 返回值:成功返回实际读取的字节数;失败返回 - 1;读到文件末尾返回 0
- 偏移特性:读取后文件偏移量会自动后移,下次读取从上次结束的位置继续(类比看书续读,无需从头重读)
(2)使用示例
cpp
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int fd = open("a.txt", O_RDONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
char buf[1024] = {0}; // 数据缓冲区
ssize_t read_len = read(fd, buf, sizeof(buf) - 1); // 预留1个字节存结束符
if (read_len == -1) {
perror("read失败");
close(fd);
return 1;
} else if (read_len == 0) {
printf("已读到文件末尾,无数据可读\n");
close(fd);
return 0;
}
printf("读取到%d个字节,内容:%s\n", (int)read_len, buf);
close(fd);
return 0;
}
6. write () 函数(写入文件内容)
(1)核心特性
- 函数功能:将缓冲区中的数据写入指定文件描述符对应的文件
- 返回值:成功返回实际写入的字节数,失败返回 - 1
- 偏移特性:默认情况下,写入后文件偏移量自动后移,下次写入从文件末尾继续
(2)使用示例
cpp
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int fd = open("b.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open失败");
return 1;
}
const char* content = "Hello Linux File Operation!";
ssize_t write_len = write(fd, content, strlen(content)); // 写入数据
if (write_len == -1) {
perror("write失败");
close(fd);
return 1;
}
printf("成功写入%d个字节\n", (int)write_len);
close(fd);
return 0;
}
7. close () 函数(关闭文件)
- 函数原型:
int close(int fd); - 核心功能:关闭指定文件描述符对应的文件,释放文件描述符、
struct file结构体及相关内核资源 - 返回值:成功返回 0,失败返回 - 1(需注意:关闭无效描述符会导致错误)
8. 父子进程的文件共享特性
| 场景 | 文件关联情况 | 文件偏移量特性 |
|---|---|---|
| 父进程先 open,后 fork () | 父子进程共享同一个struct file结构体和文件表,对应同一个物理文件 |
共享文件偏移量(一方修改,另一方立即可见) |
| 先 fork (),后各自 open 同一文件 | 父子进程拥有独立的struct file结构体和文件表,虽指向同一物理文件,但彼此独立 |
不共享文件偏移量(各自维护,互不影响) |
9. 文件复制(普通文件 / 图片等)
(1)核心原理
通过open()分别打开源文件(只读)和目标文件(只写 + 创建 + 清空),通过read()循环读取源文件数据到缓冲区,再通过write()将缓冲区数据写入目标文件,完成字节级复制(支持所有文件类型)。
(2)使用示例(图片复制)
cpp
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#define BUF_SIZE 1024 // 缓冲区大小,提升复制效率
int main() {
// 打开源文件(图片)和目标文件
int src_fd = open("source.jpg", O_RDONLY);
int dest_fd = open("target.jpg", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (src_fd == -1 || dest_fd == -1) {
perror("open失败");
close(src_fd);
close(dest_fd);
return 1;
}
char buf[BUF_SIZE];
ssize_t read_len;
// 循环读取-写入,直到读到文件末尾
while ((read_len = read(src_fd, buf, BUF_SIZE)) > 0) {
write(dest_fd, buf, read_len); // 写入与读取字节数一致的数据
}
if (read_len == -1) {
perror("读取源文件失败");
} else {
printf("文件复制完成\n");
}
// 关闭所有文件
close(src_fd);
close(dest_fd);
return 0;
}