Linux专题五:fork函数进阶,其在内存,进程上的关系,以及进程替换

一、内存相关知识点(分页机制 + 程序地址空间)

(一)内存分页核心概念(虚拟内存与物理内存映射)

1. 页的核心定义与分类
概念 别称 含义说明 关键特性
页面 虚页面 虚拟地址空间中的最小单位,进程逻辑地址空间被划分为若干个页面 大小固定(常见 4K、8K),属于虚拟内存范畴
页框 物理页面 物理内存中的最小单位,物理内存被划分为与页面大小相同的若干个页框 与页面大小一一对应,属于实际物理内存范畴
页表 - 核心数据结构,用于记录 "页面(虚拟)" 与 "页框(物理)" 的映射关系 是操作系统实现虚拟内存到物理内存映射的关键
2**. 页的核心作用**
  1. 实现虚拟内存与物理内存的映射:让程序通过逻辑地址(页面)间接访问物理地址(页框),无需直接操作硬件物理内存
  2. 简化内存管理与分配:操作系统以页为单位分配内存,无需管理零散的字节级内存,降低管理复杂度
  3. 提高内存利用率与系统性能:通过内存共享、按需分配等机制,减少内存浪费,提升系统整体运行效率
3. fork () 的写时拷贝(Copy-On-Write,COW)机制
  • 核心原理:fork () 创建子进程时,不会立即拷贝父进程的所有页面,而是让父子进程共享所有页面(包括代码段、数据段等);以 "页" 为单位进行延迟拷贝,只有当父进程或子进程修改某一页中的任意一个字节时,操作系统才会拷贝该页面(为修改方创建独立副本);若页面始终未被修改,则父子进程永久共享该页面,无需拷贝
  • 优势:提高 fork () 效率,避免不必要的内存拷贝;节省内存资源,未修改的页面无需占用额外物理内存

(二)程序内存分区(程序地址空间)

核心分区详情(按地址从低到高)
分区名称 存储内容 关键特性
代码段 程序的可执行指令(机器码)、只读常量(如字符串常量 "hello") 内存只读(防止指令被意外修改),编译时大小固定
数据段 已初始化的全局变量、已初始化的静态变量(包括 static 修饰的局部变量) 编译时分配内存,程序运行期间一直存在(直到程序结束),内存可读写
堆区 程序运行期间动态申请的内存数据(如 malloc、new 创建的变量 / 对象) 手动分配与释放(需用 free/delete 回收,否则导致内存泄漏),内存大小不固定,可动态增长,分配效率低于栈区
栈区 函数的局部变量、函数参数、函数调用时的返回地址 自动分配与释放(函数调用时分配,函数返回时自动回收),遵循 "先进后出" 原则,内存大小固定(溢出会导致栈溢出错误)

二、进程相关知识点(僵死进程 + 孤儿进程 + wait 函数 + 系统调用与库函数区别)

1. 僵死进程(僵尸进程)

  • 核心定义:子进程先于父进程结束,父进程未及时获取子进程的退出码(未回收子进程资源),子进程的 PCB(进程控制块)无法被销毁,残留于系统中,这类子进程称为僵死进程
  • 关键特性:
    1. 退出码(return/exit 的返回值)会存储在子进程的 PCB 中
    2. 危害:占用系统 PID 资源和内存空间,大量僵死进程会导致无法创建新进程
    3. 产生主要原因:父进程未调用 wait ()/waitpid () 函数回收子进程退出码

2. 孤儿进程

  • 核心定义:父进程先于子进程结束,子进程尚未结束,这类子进程称为孤儿进程
  • 关键特性:
    1. 孤儿进程会被系统分配 "养父" 进程:老内核版本中是 init 进程(PID=1),现代内核版本中不一定是 PID=1 的进程
    2. 养父进程会负责回收孤儿进程的退出码和资源,避免孤儿进程成为僵死进程

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. 关键特性

  1. 替换后原进程的所有资源(内存数据)全部消失,新程序完全替代原进程
  2. exec 函数执行成功后,不会返回原进程(新程序直接运行);只有执行失败时,才会返回 - 1 并继续执行原进程后续代码
  3. Linux 中 exec () 参数含义:1 = 可执行程序 /shell 命令,2 = 系统调用,3 = 库函数
  4. 进程替换不改变进程 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;
}

相关推荐
lifewange2 小时前
Linux 日志查看命令速查表
java·linux·运维
AndyHeee2 小时前
【rk3576 BSP音频开发bug记录】
linux·驱动开发
A13247053122 小时前
进程管理入门:查看和控制Linux进程
linux·运维·服务器·网络·chrome·github
云和数据.ChenGuang2 小时前
openeuler下的git指令集合
linux·运维·数据库·centos
风静雪冷3 小时前
在Ubuntu上安装docker(docker engine)和docker compose
linux·ubuntu·docker
m0_485614673 小时前
Linux-Dockerfile与Docker Compose
linux·运维·docker
Ghost Face...3 小时前
DDR时序校准:写均衡与门控训练解析
linux
点亮一颗LED(从入门到放弃)3 小时前
Linux驱动之中断(9)
linux·运维·单片机
阎*水3 小时前
Ceph 分布式存储完整实践指南
linux·运维·分布式·ceph