当前位置: 首页 > news >正文

六十天Linux从0到项目搭建(第十天)(系统调用 vs 库函数/进程管理的建模/为什么进程管理中需要PCB?/exec 函数/fork原理与行为详解)

系统调用 vs 库函数:本质区别与协作关系


核心区别

特性系统调用(System Call)库函数(Library Function)
定义操作系统内核提供的 底层接口,直接操作硬件。封装系统调用的 高级函数,提供便捷功能。
权限需要切换到 内核态(高权限)。运行在 用户态(普通权限)。
性能开销大(需切换内核态)。开销小(纯用户态执行)。
稳定性影响系统全局(如文件读写)。仅影响当前进程。
例子open()read()fork()printf()fopen()malloc()

1. 系统调用(System Call)

特点

  • 直接与内核交互:是用户程序访问硬件/内核功能的唯一合法途径。

  • 通过软中断触发:如 x86 的 int 0x80 或 syscall 指令。

  • 权限提升:CPU 从用户态切换到内核态。

常见系统调用(Linux为例)

类别示例功能
文件操作open()read()打开/读取文件
进程控制fork()exec()创建进程/执行程序
内存管理brk()mmap()分配内存
网络通信socket()send()建立网络连接/发送数据

代码示例

#include <unistd.h>
int main() {
    // 直接调用系统调用 write(1=stdout, "Hello", 5)
    syscall(1, 1, "Hello", 5);  // Linux x86_64 的 write 系统调用号=1
    return 0;
}

2. 库函数(Library Function)

特点

  • 封装系统调用:提供更易用的接口(如 printf 封装 write)。

  • 纯用户态执行:不直接触发权限切换,效率更高。

  • 可能不依赖系统调用:如 strcpy() 纯内存操作,无需内核介入。

常见库函数(C标准库为例)

类别示例底层依赖的系统调用
文件操作fopen()fread()open()read()
内存管理malloc()free()brk()mmap()
字符串处理strcpy()strlen()(纯用户态)
格式化输出printf()write()

代码示例

#include <stdio.h>
int main() {
    printf("Hello");  // 库函数,内部调用 write() 系统调用
    return 0;
}

3. 协作关系

从 printf 看层级调用

用户程序调用 printf("Hello")  
  ↓  
C标准库处理格式化字符串(用户态)  
  ↓  
调用 write(1, "Hello", 5) 系统调用  
  ↓  
CPU 切换至内核态,执行内核的 write() 代码  
  ↓  
内核通过磁盘驱动写入硬件(如终端显示器)  

关键点

  1. 库函数是“包装纸”:隐藏系统调用的复杂性(如 printf 处理多种数据类型)。

  2. 系统调用是“终极手段”:只有需要硬件/内核资源时才触发。

  3. 部分库函数无需系统调用:如数学计算 sin()、字符串操作 memcpy()


为什么需要两层设计?

需求系统调用的限制库函数的优势
安全性频繁切换内核态导致性能下降。用户态执行,减少开销。
易用性原始接口复杂(如手动管理文件描述符)。提供高级抽象(如 FILE* 流)。
可移植性不同OS系统调用差异大。库函数屏蔽底层差异(如Windows/Linux的 fopen 实现不同,但接口一致)。

现实类比

  • 系统调用:像直接找银行行长办业务(严格但低效)。

  • 库函数:像通过柜台职员办业务(友好且高效,职员内部再找行长)。


总结

  1. 系统调用:内核提供的“终极接口”,权限高、开销大。

  2. 库函数:封装系统调用的“工具集”,更安全、易用。

  3. 协作模式:库函数处理复杂逻辑,必要时委托系统调用访问硬件。

记住

  • 当你调用 printf(),你是在用库函数;

  • 当 printf() 调用 write(),它是在用系统调用!

进程管理的建模:从程序到进程的转换


核心概念

1. 程序 vs 进程

程序(Program)进程(Process)
存储在磁盘上的静态二进制文件(如 /bin/ls)。程序被加载到内存后的 动态执行实例
文件 = 内容 + 属性(如权限、大小)。进程 = 代码 + 数据 + 内核数据结构

2. 操作系统的作用

  • 将程序转换为进程
    当你运行 ./a.out 时,OS 负责:

    1. 从磁盘读取可执行文件。

    2. 分配内存存放代码和数据。

    3. 创建管理进程的内核数据结构(如 task_struct)。


进程管理的建模过程

1. 磁盘中的程序(静态)

  • 本质:一个普通文件,包含:

    • 代码段(Text Segment):机器指令(如 main() 函数)。

    • 数据段(Data Segment):全局变量、静态变量。

    • 文件属性:权限、所有者、大小等(通过 ls -l 查看)。

2. 加载到内存(动态进程)

  • OS 的步骤

    1. 读取文件内容:将代码和数据加载到内存的特定区域。

    2. 创建进程控制块(PCB)

      • Linux 中为 task_struct,存储进程的所有属性。

      • 包括:进程ID(PID)、状态、优先级、内存映射、打开的文件等。

    3. 初始化执行上下文

      • 设置程序计数器(PC)指向 main() 的入口地址。

      • 分配栈空间(用于局部变量和函数调用)。

3. 进程的组成

// 进程 = 内核数据结构 + 代码/数据
struct task_struct {   // PCB(进程控制块)
    int pid;           // 进程ID
    char state;        // 运行状态(就绪、阻塞等)
    struct mm_struct *mm;  // 内存管理信息
    struct file *files;    // 打开的文件列表
    // ... 其他字段(优先级、父进程等)
};

// 代码和数据(用户空间)
.text: 机器指令(如 main())
.data: 全局变量
.heap: 动态分配的内存(malloc)
.stack: 函数调用栈

关键问题解答

1. 什么是“进程”?

  • 狭义task_struct(内核数据结构) + 代码/数据(内存中的内容)。

  • 广义:程序的一次动态执行过程,包括:

    • 执行状态(运行、就绪、阻塞)。

    • 资源占用(CPU、内存、打开的文件)。

2. 为什么需要PCB?

  • 统一管理:OS 通过 task_struct 跟踪所有进程,实现:

    • 调度:决定哪个进程获得CPU。

    • 隔离:防止进程A篡改进程B的内存。

    • 资源统计:记录CPU使用时间、内存占用等。

3. 从程序到进程的完整流程

磁盘上的 a.out 
  → OS 读取文件头(ELF格式) 
  → 分配内存空间(代码/数据/堆栈) 
  → 创建 task_struct 
  → 加入调度队列 
  → CPU 执行指令

现实类比

  • 程序:像一本食谱(静态文本)。

  • 进程:像厨师按照食谱做菜(动态过程),需要:

    • 工作台(内存):存放食材和工具。

    • 任务清单(PCB):记录做到哪一步、用了哪些资源。

  • OS:像餐厅经理,分配厨师(CPU)和厨房资源(内存)。


总结

  1. 程序是“尸体”,进程是“生命”

    • 程序是磁盘上的文件,进程是内存中的鲜活实例。

  2. PCB是进程的“身份证”

    • task_struct 让OS能管理进程的所有状态。

  3. OS是“幕后导演”

    • 默默完成程序→进程的转换,并调度资源。

为什么进程管理中需要PCB?——从Bash到子进程的完整链条


1. 为什么需要PCB?

PCB(进程控制块,如 task_struct)是操作系统的“进程管理中心”,核心作用如下:

功能具体实现类比
唯一标识进程通过 pid(进程ID)区分不同进程。像学生的学号,避免混淆。
保存进程状态记录运行/就绪/阻塞状态,供调度器决策。像任务清单上的“已完成/待处理”标记。
管理资源跟踪内存分配、打开的文件、网络连接等。像仓库的库存表。
实现进程隔离每个进程有独立的地址空间(通过 mm_struct 管理)。像银行客户的独立保险箱。
支持父子关系记录父进程 ppid,实现进程树(如 bash 是所有命令行进程的父进程)。像家族族谱。

没有PCB的后果

  • 进程无法被调度(OS不知道谁该运行)。

  • 内存泄漏(无法释放已终止进程的资源)。

  • 安全漏洞(进程可随意篡改他人内存)。


2. Bash与子进程的关系

(1) Bash本身是一个进程

  • Bash的PCB

    • 当你在终端输入命令时,Bash(/bin/bash)已作为进程运行,其PCB由OS维护。

    • 可通过 ps 查看:

      ps -ef | grep bash

      输出示例:

      ubuntu   1234  5678  0 10:00 pts/0    00:00:00 /bin/bash

(2) Bash如何创建子进程?

当你在Bash中运行 ./a.out 时:

  1. Bash调用 fork()

    • 复制当前Bash进程的PCB(生成一个子进程,继承Bash的环境变量、文件描述符等)。

    • 此时父子进程的代码执行位置完全相同(都停在 fork() 返回处)。

  2. 通过返回值分流

    • 子进程的 fork() 返回 0

    • 父进程(Bash)的 fork() 返回 子进程的PID

    • 代码示例:

      pid_t pid = fork();
      if (pid == 0) { 
          // 子进程执行流
          execvp("a.out", args);  // 加载a.out替换当前进程
      } else { 
          // 父进程(Bash)执行流
          wait(NULL);  // 等待子进程结束
      }
  3. 子进程运行目标程序

    • 子进程调用 exec() 系列函数,将自身替换为 a.out 的代码和数据。

    • 关键点

      • exec() 会替换代码段,但 保留原PCB(如 pid、打开的文件描述符)。


3. 子进程创建的核心机制

(1) fork() 的特性

特性说明
写时复制(COW)父子进程共享内存,直到一方尝试修改时才会复制(节省资源)。
执行顺序不确定由调度器决定父子进程谁先运行(可通过同步机制控制,如 wait())。
共享打开的文件子进程继承父进程的文件描述符(如终端输入/输出)。

(2) 代码分流的关键

fork();  // 执行后,从这里分裂出两个执行流

// 父子进程都会执行以下代码
if (pid == 0) {
    // 子进程专属代码
} else {
    // 父进程专属代码
}

现实类比

  • Bash:像餐厅经理(有自己的工作任务表-PCB)。

  • fork():经理复制一份自己的任务表,交给新员工(子进程)。

  • exec():新员工扔掉复制的任务表,换成具体的菜谱(a.out)。

  • wait():经理等待员工做完菜再继续自己的工作。


总结

  1. PCB是进程的“大脑”

    • 没有它,OS无法管理进程的生死、资源、状态。

  2. Bash是所有命令行进程的父进程

    • 通过 fork() + exec() 启动子进程,并通过PCB维护父子关系。

  3. fork() 的魔法

    • 复制PCB → 分流执行流 → 通过 exec() 加载新程序。

4 exec 函数家族详解:替换当前进程的“灵魂”

exec 是操作系统提供的一组系统调用,用于 将当前进程的代码和数据替换为一个新程序,但保留原有进程的PID、文件描述符等属性。可以理解为“进程的灵魂置换术”。


核心功能

  • 不创建新进程:仅在当前进程内加载新程序(与 fork() 不同)。

  • 完全替换:原程序的代码、数据、堆栈被新程序覆盖。

  • 继承环境:保留原进程的PID、打开的文件描述符、信号处理等。


exec 函数家族(6个变体)

均定义在 <unistd.h> 中,根据参数传递方式不同分为:

函数原型参数传递方式搜索路径适用场景
int execl(const char *path, const char *arg0, ..., NULL)列表传参(可变参数)需完整路径参数固定且较少时
int execle(const char *path, const char *arg0, ..., NULL, char *const envp[])列表传参 + 自定义环境变量需完整路径需指定环境变量
int execlp(const char *file, const char *arg0, ..., NULL)列表传参自动搜索 PATH调用系统命令(如 ls
int execv(const char *path, char *const argv[])数组传参需完整路径参数动态生成时
int execvp(const char *file, char *const argv[])数组传参自动搜索 PATH最常用(灵活+自动寻路)
int execvpe(const char *file, char *const argv[], char *const envp[])数组传参 + 自定义环境变量自动搜索 PATH需自定义环境变量且自动寻路

使用示例

1. 基本用法(execlp 调用 ls

#include <unistd.h>
#include <stdio.h>

int main() {
    printf("Before exec\n");
    // 执行 ls -l /, 自动搜索PATH
    execlp("ls", "ls", "-l", "/", NULL);  // 参数列表必须以NULL结尾
    printf("This line won't be reached!\n");  // exec成功时不会返回
    return 0;
}

输出

Before exec
total 16
drwxr-xr-x 2 root root 4096 Jan 1  1970 bin
...

2. 动态参数(execvp 调用自定义命令)

#include <unistd.h>

int main() {
    char *args[] = {"echo", "Hello, exec!", NULL};  // 参数数组
    execvp("echo", args);  // 自动搜索PATH中的echo
    return 0;
}

输出

Hello, exec!

3. 配合 fork() 创建子进程

#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {  // 子进程
        execlp("sleep", "sleep", "5", NULL);  // 子进程替换为sleep 5
    } else {  // 父进程
        wait(NULL);  // 等待子进程结束
        printf("Child finished sleeping.\n");
    }
    return 0;
}

关键注意事项

  1. 成功时无返回值exec 调用成功后,原进程的代码已被替换,后续代码不会执行。

  2. 失败时返回 -1:需检查错误(如文件不存在、无权限):

    c

    复制

    if (execvp("nonexistent", args) == -1) {
        perror("execvp failed");
    }
  3. 参数列表必须以 NULL 结尾:否则会导致未定义行为。

  4. 环境变量继承:默认继承父进程的环境变量,可用 execle 或 execvpe 自定义。


现实类比

  • fork():克隆一个完全相同的你(复制PCB)。

  • exec():把你的大脑换成爱因斯坦的(保留身体和身份证,但思想和能力全变)。


总结

  • 何时用:需要运行另一个程序,但不想创建新进程时(如Shell执行命令)。

  • 怎么选

    • 参数固定 → execlp

    • 参数动态 → execvp

    • 需自定义环境 → execvpe

  • 经典组合fork() + exec() + wait() 实现进程创建与程序加载。

5. fork() 的原理与行为详解

1. fork() 的核心功能

fork() 是Linux/Unix系统的一个系统调用,用于 创建一个与当前进程几乎完全相同的子进程。其核心行为包括:

  • 复制父进程的PCB(task_struct:子进程继承父进程的进程属性(如文件描述符、信号处理等)。

  • 复制内存空间:代码段、数据段、堆栈等(实际采用 写时拷贝 优化,见下文)。

  • 分配新的PID:子进程获得唯一的进程ID。

2. 代码与数据的处理
  • 代码段(只读)
    父子进程 共享同一份代码(因为代码不可修改,无需复制)。

  • 数据段(写时拷贝, Copy-On-Write, COW)

    • 初始时,父子进程 共享同一份物理内存(仅标记为“只读”)。

    • 当任一进程尝试 修改数据(如全局变量),操作系统会触发缺页异常,自动复制该内存页给修改方,实现隔离。

    • 优势:避免无意义的拷贝,提升性能。

3. 进程的独立性
  • 内存隔离:父子进程的修改互不影响(得益于COW)。

  • 调度独立:由操作系统决定父子进程的执行顺序。

4. 两个返回值的奥秘

fork() 的“一次调用,两次返回”是理解的关键:

pid_t pid = fork();  // 从这里分裂出两个执行流
  • 父进程fork() 返回 子进程的PID(>0)。

  • 子进程fork() 返回 0

  • 失败时:返回 -1(如进程数超限)。

底层原理

  • fork() 执行完成后,内核已将父进程的上下文(寄存器、PC等)复制到子进程。

  • 在 返回前的瞬间,父子进程的指令指针(PC)均指向 fork() 的下一条指令。

  • 返回值差异:内核通过检查进程的PCB(如 task_struct)决定返回哪个值。

5. 代码示例与分流
#include <unistd.h>
#include <stdio.h>

int main() {
    int x = 100;  // 父子进程初始共享x
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
    } else if (pid == 0) {  // 子进程
        x = 200;  // 触发写时拷贝,子进程的x独立
        printf("Child: x=%d\n", x);  // 输出 200
    } else {  // 父进程
        printf("Parent: x=%d\n", x); // 输出 100
    }
    return 0;
}
6. 关键结论
  • fork() 的本质:复制PCB + 虚拟内存映射(COW优化)。

  • 返回值设计:通过区分父子进程的PCB实现逻辑分流。

  • 独立性保障:写时拷贝确保进程间数据修改互不干扰。

类比

  • fork() 像细胞分裂,子细胞继承母细胞的“记忆”(PCB),但后续行为可独立发展。

  • 返回值像“出生证明”,子进程拿到“0”(新生儿标识),父进程拿到“出生登记号”(PID)。

pc均指向fork,pc是什么

PC(Program Counter)是什么?

PC程序计数器,Program Counter)是CPU中的一个 特殊寄存器,用于存储 当前正在执行的指令的内存地址。在x86架构中,它通常被称为 EIP(32位)或 RIP(64位),而在ARM架构中则直接称为 PC


PC 的核心作用

  1. 指向下一条要执行的指令

    • CPU 根据 PC 的值从内存中读取指令并执行。

    • 每执行完一条指令,PC 会自动递增(或跳转,如分支、函数调用)。

  2. 在 fork() 时的关键行为

    • 当调用 fork() 时,父子进程的 PC 都指向 fork() 的下一条指令

    • 这是 fork() 能“分裂”出两个执行流的根本原因。


fork() 与 PC 的关系

1. fork() 的执行流程

pid_t pid = fork();  // 执行到这里时,CPU的PC指向这一行
  • fork() 执行前

    • PC 指向 fork() 的调用指令(如 call fork)。

  • fork() 执行后

    • 父进程PC 指向 fork() 的下一条指令(即 if (pid == 0))。

    • 子进程PC 同样 指向 fork() 的下一条指令(因为子进程完全复制了父进程的上下文,包括 PC)。

2. 为什么 fork() 有两个返回值?

  • 内核的魔法

    • fork() 在 内核态 完成进程复制后,会 手动修改父子进程的返回值

      • 父进程的返回值:子进程的 pid

      • 子进程的返回值:0

  • PC 的作用

    • 父子进程的 PC 相同,因此都会从 fork() 的下一条指令继续执行。

    • 但返回值不同,导致代码分流(通过 if (pid == 0) 判断)。


示例代码分析

#include <unistd.h>
#include <stdio.h>

int main() {
    printf("Before fork\n");
    pid_t pid = fork();  // PC 指向这一行
    // fork() 返回后,PC 指向下一行(父子进程相同)
    
    if (pid == 0) {
        printf("Child: PID=%d\n", getpid());
    } else {
        printf("Parent: Child's PID=%d\n", pid);
    }
    return 0;
}

执行流程

  1. 父进程

    • fork() 返回子进程的 pid,进入 else 分支。

  2. 子进程

    • fork() 返回 0,进入 if 分支。

  3. 关键点

    • 父子进程的 PC 在 fork() 返回后指向同一位置,但返回值不同导致逻辑分流。


现实类比

  • PC 像书签:标记你当前读到书的哪一页。

  • fork() 像复印书

    • 复印后,你和复印本的书签都停在原书的同一页。

    • 但你可以选择继续读(父进程),或让复印本自己读(子进程)。


总结

  • PC 是指令指针:决定CPU下一步执行哪条指令。

  • fork() 复制 PC:父子进程从同一位置继续执行,但返回值不同。

  • 分流的关键:通过 if (pid == 0) 判断当前是父进程还是子进程。

相关文章:

  • 【Linux加餐-网络命令】
  • 数仓架构告别「补丁」时代!全新批流一体 Domino 架构终结“批流缝合”
  • vue中使用defineModel简化defineProps和defineEmits的用法
  • Node.js Express 处理静态资源
  • linux 抓图机器资源不足,排查和删除图片文件
  • Java | 基于 ThreadLocal 实现多客户端访问设备的 REST 请求下发
  • 量子计算:开启信息时代新纪元的钥匙
  • 阀门流量控制系统MATLAB仿真PID
  • 从 YOLO11 模型格式导出到TF.js 模型格式 ,环境爬坑,依赖关系已经贴出来了
  • Python中multiprocessing的使用详解
  • git push的时候出现无法访问的解决
  • MinGW下编译ffmpeg源码时生成compile_commands.json
  • 微信小程序报错:600001 ERR_CERT_AUTHORITY_INVALID 的问题排查及解决
  • 区块链技术在投票系统中的应用:安全、透明与去中心化
  • (!常识!)C++中的内存泄漏和野指针——如何产生?如何避免?解决方案?基本原理?面试常问重点内容?
  • Springbean(二)@Component及其派生注解自动注入(2)使用注意和加载问题
  • JSON是什么
  • 【Git “reset“ 命令详解】
  • 论文浅尝 | C-ICL:用于信息抽取的对比式上下文学习(EMNLP2024)
  • 淘宝获取商品sku详情API接口如何调用?
  • 全国人大常委会关于授权国务院在中国(新疆)自由贸易试验区暂时调整适用《中华人民共和国种子法》有关规定的决定
  • 国务院安委办、应急管理部进一步调度部署“五一”假期安全防范工作
  • 看展览|建造上海:1949年以来的建筑、城市与文化
  • 伊朗内政部长:港口爆炸由于“疏忽”和未遵守安全规定造成
  • 柴德赓、纪庸与叫歇碑
  • 劳动最光荣!2426人受到表彰