本文尚未完成,但近期不会补充完整。
这里记录一下还需要写的部分:
- 实验代码与解析
实验3:Linux 进程管理
基本原理与知识点
-
读端与写端的区分
piep()系统调用中:-
fd[0] 是读端
-
fd[1] 是写端
-
-
close() 怎么理解
通俗易懂的说,我们可以把文件描述符理解为电话号码。
1 2pipe(fd) // 创建管道;相当于申请了一对专线电话, // 其中一条只能听(fd[0]),一条只能说(fd[1])假设
pipe(fd)后,fd[0]=3,fd[1]=4。子进程创建后,会复制父进程的整个”通讯录“,而不是新建管道。
也就是说在子进程中,依然是fd[0]=3,fd[1]=4。
让我们回到
close(),可以这样理解,一个进程执行close(fd[1]),意味着它删除了这条拿来说的电话线(fd[1] 是写端),此后它不能再往这个管道中写数据,只能读。但值得注意的是,父进程
close(fd[1])后,子进程不受影响,子进程依然可以写数据。一句话总结:每个进程有自己的 fd,但指向同一个管道。
close()只是减少该管道在这个进程中的引用。 -
read() 函数返回什么?
read()的典型用法如下所示:1ssize_t n = read(fd, buf, count);返回值 含义 场景 > 0 实际读取的字节数 正常情况,可能小于 count(短读) 0 EOF(文件结束) 所有写端都已关闭 -1 出错 被信号中断、非阻塞无数据等 -
创建管道时,读写两端自动打开,无需
open() -
文件描述符(fd)是啥?
文件描述符是进程级别的”文件句柄“,是一个非负整数(0,1,2,3,…)。
可以这样理解,一个进程中有一张表格,列举了进程打开的I/O资源(文件、管道、设备等),这个文件描述符可以理解为这个表格的整数索引。
进程通过编号(文件描述符号),可以让内核操作资源,无需知道文件在内核中的复杂结构。
-
fd 是进程私有的,比方说有两个进程,进程 A 的 fd=3 可能指向管道;进程 B 的 fd=3 可能指向完全不同的文件。
-
fork()创建子进程会复制 fd,但指向同一个内核对象 -
fd值有常见的约定:
fd 值 含义 对应 C 常量 0 标准输入(stdin) STDIN_FILENO 1 标准输出(stdout) STDOUT_FILENO 2 标准错误(stderr) STDERR_FILENO ≥3 用户打开的文件/管道
-
-
pipe(fd)做了什么?在管道实验中,我们常会看到:
1 2int fd[2]; pipe(fd);fd初始化时是空的,
pipe(fd)后,创建一个新的管道,并分配两个文件描述符,对管道的读端与写端。这里我们能进一步理解文件描述符的本质:它就是一个数,用来标识一个内核资源。
-
写端、读端是否需要关闭?
一句话讲明:不关闭不需要的端会导致
read()永远阻塞。-
写数据的进程不关闭读端(fd[0])
能写入,不受影响。
当所有写端都关闭后,读端应该收到 EOF(
read()返回 0),但如果你自己还开着读端,系统认为“还有进程可能写入”,读端会永远阻塞在read()上。 -
读数据的进程不关闭写端(fd[1])
能读取,如果管道里有数据就可以正常读。
当管道读空后,
read()不会返回0(EOF),而是阻塞等待,因为系统看到你还开着写端,认为“迟早还要写数据进来”。
一句话解释:管道是单向通信。
-
-
关闭写/读端,减少引用计数
这个计数可以涉及两方面:
-
文件结构体的引用计数
调用
pipe(fd)时,内核会创建:-
两个
struct file对象(代表管道的两端) -
一个底层的
pipe_inode_info(管道的实际缓冲区)
每个进程持有文件描述符(fd)时,内核中的
struct file的引用计数(f_count)会增加:-
fork()时,子进程继承 fd,引用计数 +1 -
close()时,引用计数 -1
当这个引用计数降到0,内核才释放这个文件结构体。
-
-
管道的“写者计数”(Writer Count)
管道内部同样存在写端计数器。内核需要知道:“还有没有进程要往这个管道写数据?”
调用
close(fd[1])时,内核执行:-
减少 file 结构体的引用计数
-
检查这是否是最后一个写端。
如果是,唤醒阻塞的读进程,让它们知道“不会再有数据了”。
-
我们看到一个反面教材:
-
父进程:read(fd[0]),阻塞等待数据
-
子进程:write(fd[1]),写入消息
-
子进程:close(fd[1]),但 fd[0] 还开着!!
此时,虽然子进程写完且关闭了写端,但还持有读端,内核认为:
还有进程持有读端,它可能 fork 出新的子进程来继续写入
因此,计数不会变成0,父进程的
read()会永远阻塞。 -
-
管道阻塞场景
既然说到阻塞,我们来看几种阻塞场景
-
读空管:当管道中没有数据时,
read会阻塞,知道有进程写数据才被唤醒 -
写满管:管道有容量限制(通常是64KB)。管道满时,写进程必须等读进程“腾出空间”
-
多写一端关闭:当有多个写进程时,只要还有一个写进程持有 fd[1],读端会认为”还可能有人写“,继续阻塞;
read返回 0,表示所有写端都关闭了。 -
阻塞是线程安全的,唤醒是有序的:
这个场景稍微复杂些:
-
父进程开始读,此时管道是空的,阻塞ing
-
子进程依次分别写消息
-
情况出现了!! 父进程读到子进程1的数据,直接返回,结束。
read是流式读取,有数据就读。
如果需要父进程读完全部数据,必须让子进程全部写完,再读。
这也是我们实验的重点之一。
-
-
-
POSIX 有名信号量
核心API(四个动作)
函数 作用 关键参数 sem_open(name, flags, mode, value)创建或打开 name必须以/开头(如"/my_sem");O_CREAT创建;value初始值sem_wait(sem)P 操作(等待) 值 > 0 则减 1 并返回;值 = 0 则阻塞 sem_post(sem)V 操作(释放) 值加 1;若有进程等待,唤醒其中一个 sem_close(sem)关闭句柄 进程退出时自动调用,但不删除信号量本身 sem_unlink(name)删除信号量 从系统中移除;若其他进程正持有,待其 close后销毁这里展开讲讲
sem_open的第2,3个参数:-
标志位:控制打开方式,常用按位或(
|)组合标志 含义 效果 O_CREAT创建模式 信号量不存在则创建,存在则直接打开(最常用) O_EXCL排他创建 必须与 O_CREAT配合使用,若信号量已存在则报错(防止重复初始化)O_RDWR读写打开 隐式默认,无需显式指定 1 2sem_open("/my_sem", O_CREAT | O_EXCL, 0644, 1); // 强制新建,已存在则失败(适合初始化进程) sem_open("/my_sem", O_CREAT, 0644, 1); // 通用模式:没有就建,有就直接用 -
权限位:仅当实际创建新信号量时生效(如果信号量已存在,此参数被忽略)
采用 Unix 标准权限格式,通常写为 八进制数。
有名信号量在内核中对应一个特殊文件(/dev/shm/sem.my_sem)
0(特殊位)6(文件所有者权限)4(组用户权限)4(其他用户权限)
0644表示:创建者可读写,其他进程只能读(无法修改信号量值,但可查询状态)
常见取值:
-
0644:普通共享(所有用户可读,仅所有者可写) -
0666:完全共享(所有用户可读写,风险较高) -
0600:私有(仅创建者自身进程可访问)
-
典型使用流程
1 2 3 4 5 6 7 8 9 10 11// 1. 创建/打开(类似 open()) sem_t *sem = sem_open("/project_lock", O_CREAT, 0644, 1); // 2. 临界区保护 sem_wait(sem); // 进入,获取锁 // ... 访问共享资源 sem_post(sem); // 退出,释放锁 // 3. 清理(必须显式删除!) sem_close(sem); sem_unlink("/project_lock"); // 通常由最后一个进程或初始化进程执行值得注意的:有名信号量生命周期独立于进程,即使创建进程仍存活,其他进程可继续打开使用。
这也是和无名信号量(
sem_init)的区别之一,无名信号量会随进程结束自动销毁,它仅适用于fork后的父子进程。 -
题目2:实现一个管道通信程序
由父进程创建一个管道,然后再创建3个子进程,并由这三个子进程利用管道与父进程之间进行通信:子进程发送信息,父进程等三个子进程全部发完消息后再接收信息。
通信的具体内容可根据自己的需要随意设计,要求能试验阻塞型读写过程中的各种情况,测试管道的默认大小,并且要求利用Posix信号量机制实现进程间对管道的互斥访问。
运行程序,观察各种情况下,进程实际读写的字节数以及进程阻塞唤醒的情况。
我们先来通过一些小demo看看管道的基本使用方法
值得注意的:demo中有一些不严谨的写法(如写进程完成写后,没有关闭写端,而是直接return 0),或许不会导致运行问题(进程退出时会自动清理),但在正式实验中不可以这样写。
demo1:基础管道(验证单向通信)
父进程写,子进程读
|
|
子进程:等待数据…
(阻塞2秒)
父进程:已发送
子进程:读到 5 字节:Hello
demo2:管道容量测试(观察阻塞)
管道满时,写阻塞
原子性写入限制:要么都写,要么都不写,没有部分写
|
|
已写入:4096 bytes
已写入:8192 bytes
已写入:12288 bytes
……
已写入:65536 bytes
write: Resource temporarily unavailable
demo4:数据混杂问题
3个子进程同时写,会导致数据混杂,这也是为什么正式实验需要引入信号量机制
|
|
子进程 1 写完
子进程 0 写完
实际读取 300 字节,结果:012102100212101020122010212011201201201020210210021021021020101201201221002101212021021012021010212021021012012012012210102120021120201210212102012012012012210021021120210201021201201021021200122101202100120120120120121002112010210201201210201102102021012012010120122101202102101210201220120102120122
子进程 2 写完
(多进程并发时,子进程实际开始、打印、结束顺序是混杂的)
demo4:POSIX 有名信号量基础
|
|
子进程:等待信号量…
子进程:获得锁,持有3秒…
(阻塞3秒)
父进程:等待信号量…
(等待一段时间)
子进程:释放锁
父进程:获得锁
注意!! 这个源代码的编译命令不一样:
|
|
-pthread 是 GCC 的 POSIX 线程支持选项,它告诉编译器:
-
预处理阶段:定义
_REENTRANT宏(表示可重入代码) -
链接阶段:自动链接
libpthread线程库
实验代码
|
|