linux2.2系统编程-进程管理
linux2.2系统编程-进程管理
每个进程都有独立的地址空间、系统资源(如文件描述符、信号处理器等)。因此,进程之间的切换开销较大,因为需要保存当前进程的状态,并加载下一个进程的状态。进程的实现并不依赖多核并行计算,而是通过时间片实现。时间片是指通过时分复用为每个进程分配较为完整的虚拟内存。
线程是对进程的分割,避免进程的资源浪费。线程是轻量级的,它属于某个进程,并且与同一进程中的其他线程共享大多数资源(如内存地址空间、打开的文件等),但每个线程有自己的执行上下文(如寄存器状态、堆栈指针等)。由于线程间共享资源,线程间的切换比进程间的切换要快得多。
1 进程与线程的区别
进程(Process)和线程(Thread)都是操作系统进行运算调度的基本单位,但它们之间有几个关键的区别:
资源拥有
- 进程:每个进程都有独立的地址空间、系统资源(如文件描述符、信号处理器等)。因此,进程之间的切换开销较大,因为需要保存当前进程的状态,并加载下一个进程的状态。
- 线程:线程是轻量级的,它属于某个进程,并且与同一进程中的其他线程共享大多数资源(如内存地址空间、打开的文件等),但每个线程有自己的执行上下文(如寄存器状态、堆栈指针等)。由于线程间共享资源,线程间的切换比进程间的切换要快得多。
独立性
- 进程:一个进程的执行不会直接受到其他进程的影响。如果一个进程崩溃,通常不会影响到其他进程。
- 线程:线程不是完全独立的,同属一个进程的所有线程共享该进程的资源。如果一个线程出现错误,可能会导致整个进程失败。
通信
- 进程:进程间通信(IPC, Inter-Process Communication)较为复杂,可能需要使用专门的机制如管道、套接字、消息队列或共享内存以及信号等来实现进程间的数据交换,特殊的信号量与文件锁理论上也可用于进程间通信,但不是首选。
- 线程:由于线程共享同一个进程的地址空间,它们可以直接通过读写相同的内存区域来进行通信,这使得线程间通信更为简单高效。
2 进程控制块
进程控制块(Process Control Block,简称PCB)是操作系统用来管理进程的数据结构。每个进程都有一个与之关联的PCB,它包含了操作系统需要知道的关于该进程的所有信息。PCB对于进程的创建、调度、同步和终止等操作至关重要。
PCB中通常包含的信息有:
标识信息:
- 进程ID(
PID
):用于唯一标识一个进程,父进程(PPID
)。 - 用户ID(
UID
)和组ID(GID
):表示拥有该进程的用户及其所属组。
- 进程ID(
状态信息:
基本状态:可以是创建、就绪、运行、等待(阻塞)或终止。这反映了进程当前是否在CPU上执行、准备执行还是等待某些事件发生。
创建态:系统正在为其分配内存等资源,未进入就绪态。
就绪态:等待调度器分配CPU进行计算。
- 运行态:CPU执行计算。
- 阻塞态:进程由于等待中断等事件被挂起,期间基本不消耗CPU计算资源。
- 终止态:进程结束或被杀死,系统回收资源时。
查看PCB:
1 | cat/proc/<pid>/status |
PCB的作用
- 进程管理:操作系统通过PCB来管理和跟踪系统中的所有进程。
- 进程切换:当操作系统决定切换到另一个进程时,它会保存当前进程的PCB,并加载新进程的PCB以恢复其执行环境。
- 调度决策:调度器根据PCB中的信息(如优先级)来选择下一个应该运行的进程。
- 资源分配与回收:PCB帮助操作系统确定为进程分配哪些资源以及何时回收这些资源。
3 Linux进程管理命令
3.1 ps -aux
或ps -ef
(简略输出)
1 | serenitatis@shumeipai:~/linux-project $ ps -aux |
USER:
- 显示运行该进程的用户名称。这通常是启动进程的用户账号。
PID (Process ID):
- 每个进程都有一个唯一的标识符,称为进程ID(PID)。这是操作系统用来识别和管理各个进程的关键值。
%CPU:
- 表示进程占用的CPU时间百分比。它反映了自上次更新以来,该进程使用的CPU资源占总可用CPU资源的比例。
%MEM:
- 显示进程使用的物理内存百分比。它表示的是相对于系统总RAM大小,该进程所占的份额。
VSZ (Virtual Memory Size):
- 进程虚拟内存大小,以KB为单位。这包括了程序代码、数据段以及所有已映射文件的大小。注意,这个值可能比实际使用的物理内存量要大,因为它还包含了未分配的虚拟地址空间。
RSS (Resident Set Size):
- 实际驻留在主存中的进程内存大小,以KB为单位。RSS是进程当前正在使用的物理内存量,不包括已经被交换出去的部分。
TTY (Teletype):
- 如果进程是通过终端启动的,则显示与之关联的终端设备名(例如,pts/0, tty1等)。对于守护进程或其他非交互式进程,这里可能会显示”?”或”-“.
STAT (Status):
进程的状态代码。常见的状态有:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45进程状态详解
D: 不可中断的休眠态
进程处于等待某个资源(通常是 I/O 操作)的状态,此时无法被中断,直到资源可用。
R: 运行态和就绪态
进程正在运行或者可以运行(在就绪队列中等待 CPU 资源)。
S: 可中断的休眠态
进程正在等待某个事件完成,可以被信号中断。
T: 停止(暂停)状态
进程被作业控制信号停止,例如通过 `Ctrl+Z` 暂停。
t: 被调试器暂停
进程在调试期间被调试器暂停。
W: 分页状态(自 2.6 内核起不再有效)
过去用于表示进程在等待分页,但该状态已过时。
X: 死亡态
进程已经死亡,这个状态几乎不会看到,因为它会很快消失。
Z: 僵尸态
进程已终止,但其父进程尚未回收它的资源,导致它以僵尸状态存在。
### 进程的附加状态
<: 优先级较高
进程的优先级较高,可能会抢占其他进程的资源,对其他用户不友好。
N: 优先级较低
进程的优先级较低,运行时对其他用户较为友好。
L: 内存锁定
进程的某些内存页被锁定在内存中,通常用于实时操作或自定义 I/O 操作。
s: 会话组长
进程是会话组的组长,负责管理一组相关进程。
l: 多线程进程
进程中包含多个线程,通常使用 `CLONE_THREAD` 实现,如 NPTL pthreads。
+: 前台进程
进程属于前台进程组,通常与用户交互较多。
START:
- 显示进程开始的时间。可以是日期或者具体的启动时间戳。
TIME:
- 累计CPU时间,即从进程启动至今消耗的CPU时间总量,格式为分钟:秒数。
COMMAND:
- 显示启动进程的命令行及其参数。有时为了节省空间,较长的命令可能会被截断;在这种情况下,你可以使用其他方法(如
ps -f
)来查看完整的命令行。
- 显示启动进程的命令行及其参数。有时为了节省空间,较长的命令可能会被截断;在这种情况下,你可以使用其他方法(如
一般结合grpe
使用
1 | ps -aux |grep <name> |
3.2 htop
类windows任务管理器
3.3 kill向进程发送信号
1 | kill |
4 Linux进程管理c接口
4.1 fork创建新子进程
fork创建子进程,先复制父进程复制完成后是独立的进程。
1 | SYNOPSIS |
注意创建子进程得到的数量
1 |
|
输出结果8个#
4.2 父子进程的内存拷贝
在Linux系统中,当一个父进程通过fork
系统调用创建子进程时,子进程会获得父进程的一个拷贝。这种拷贝遵循“写时拷贝”的原则。这意味着在创建子进程时,父进程和子进程最初共享相同的内存空间,直到其中一个进程试图修改内存内容时,才会进行实际的内存拷贝。
1 |
|
output:
1 | 我是父进程.. |
4.3 查看当前PID
1 | NAME |
example :
1 |
|
output:
1 | pid of parent:20147 |
一般来说子进程的pid会在父节点的基础上加1。
4.4 主动退出进程exit
exit是直接退出当前进程,不区别是子进程还是父进程。所以可能会造孤儿节点,孤儿节点只能由系统进程(PID = 1
)回收。
1 | int main() { |
4.5 回收子进程wait
wait
和 waitpid
是用于回收子进程的系统调用,主要用于父进程等待子进程结束并获取其退出状态。
4.5.1 wait
函数
功能: 等待任意一个子进程结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16NAME
wait, waitpid, waitid - wait for process to change state
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
pid_t wait(int *_Nullable wstatus);
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
/* This is the glibc and POSIX interface; see
NOTES for information on the raw system call. */
参数:
status
: 用于存储子进程的退出状态。
返回值:
- 成功时返回结束的子进程的 PID。
- 失败时返回 -1。
特点:
- 如果没有子进程结束,父进程会阻塞。
- 如果有多个子进程,任意一个结束都会返回。
4.5.2 waitpid
函数
- 功能: 等待指定的子进程结束。
- 参数:
pid
:> 0
: 等待指定 PID 的子进程。-1
: 等待任意子进程,类似wait
。0
: 等待与父进程同组 ID 的任何子进程。< -1
: 等待组 ID 等于pid
绝对值的任何子进程。
status
: 用于存储子进程的退出状态。options
: 控制行为,常用选项:WNOHANG
: 非阻塞模式,没有子进程结束时立即返回。WUNTRACED
: 返回已停止的子进程状态。
- 返回值:
- 成功时返回结束的子进程的 PID。
- 如果使用
WNOHANG
且没有子进程结束,返回 0。 - 失败时返回 -1。
- 特点:
- 可以指定等待的子进程。
- 支持非阻塞模式。
4.5.3 示例代码
1 |
|
4.5.4 总结
wait
用于等待任意子进程结束。waitpid
更灵活,可以指定子进程并支持非阻塞模式。- 两者都用于回收子进程资源,防止僵尸进程。
4.6 exe
函数簇利用外部应用创建进程
exec
函数簇是一组用于替换当前进程映像的系统调用。它们会将当前进程的代码段、数据段、堆栈等替换为新的程序文件,并从新程序的入口点开始执行。与 fork
不同,exec
不会创建新进程,而是替换当前进程的内容。
exec
函数簇通常与 fork
结合使用,用于创建新进程并运行外部程序。
4.6.1 exec
函数簇的常见函数
以下是 exec
函数簇的常见函数:
函数名 | 描述 |
---|---|
execl |
参数列表形式,需提供完整路径名。 |
execlp |
参数列表形式,会在 PATH 环境变量中查找可执行文件。 |
execle |
参数列表形式,可指定环境变量。 |
execv |
参数数组形式,需提供完整路径名。 |
execvp |
参数数组形式,会在 PATH 环境变量中查找可执行文件。 |
execvpe |
参数数组形式,可指定环境变量。 |
4.6.2 函数原型
1 |
|
4.6.3 参数说明
path
: 可执行文件的完整路径。file
: 可执行文件名,会在PATH
环境变量中查找。arg
: 程序的命令行参数,第一个参数通常是程序名。argv
: 参数数组,以NULL
结尾。envp
: 环境变量数组,以NULL
结尾。
4.6.4 返回值
- 成功时,
exec
函数不会返回(因为当前进程的映像已被替换)。 - 失败时,返回
-1
,并设置errno
。
4.6.5 示例代码
4.6.5.1 示例 1:使用 execl
运行 ls
命令
1 |
|
4.6.5.2 示例 2:使用 execvp
运行 ls
命令
1 |
|
4.6.5.3 示例 3:结合 fork
和 exec
创建新进程
1 |
|
4.6.6 关键点
exec
不会创建新进程:- 它只是替换当前进程的映像。
- 如果需要创建新进程,通常先调用
fork
,然后在子进程中调用exec
。
- 参数传递:
- 使用
execl
或execle
时,参数以列表形式传递。 - 使用
execv
或execvp
时,参数以数组形式传递。
- 使用
- 环境变量:
execle
和execvpe
可以指定环境变量数组。- 其他函数会继承当前进程的环境变量。
- 错误处理:
- 如果
exec
失败,当前进程会继续执行,因此需要检查返回值并处理错误。
- 如果
4.6.7 总结
exec
函数簇用于替换当前进程的映像,运行外部程序。- 通常与
fork
结合使用,创建新进程并运行外部程序。 - 不同的
exec
函数适用于不同的场景(如是否搜索PATH
、是否指定环境变量等)。
4.7 popen
管道回收外部应用的返回值
popen
是一个标准库函数,用于创建一个管道并启动一个外部应用程序。通过 popen
,你可以与外部应用程序进行通信,并读取其输出或向其输入数据。popen
的返回值是一个文件指针(FILE*
),可以通过标准 I/O 函数(如 fread
、fgets
等)读取或写入数据。
popen
特别适合用于执行命令行工具并捕获其输出。
4.7.1 popen
函数原型
1 |
|
4.7.2 参数说明
command
:- 要执行的命令行命令。
- 例如:
"ls -l"
、"grep hello"
等。
type
:- 指定管道的类型:
"r"
:以读模式打开管道,读取外部应用程序的输出。"w"
:以写模式打开管道,向外部应用程序输入数据。
- 指定管道的类型:
- 返回值:
- 成功时,返回一个
FILE*
指针,用于读取或写入数据。 - 失败时,返回
NULL
,并设置errno
。
- 成功时,返回一个
4.7.3 pclose
函数
- 功能:关闭由
popen
打开的管道,并等待外部应用程序结束。 - 参数:
stream
:popen
返回的FILE*
指针。
- 返回值:
- 成功时,返回外部应用程序的退出状态。
- 失败时,返回
-1
。
4.7.4 示例代码
4.7.4.1 示例 1:读取外部应用程序的输出
以下代码使用 popen
执行 ls -l
命令,并读取其输出:
1 |
|
4.7.4.2 示例 2:向外部应用程序输入数据
以下代码使用 popen
向 grep
命令输入数据,并读取其输出:
1 |
|
4.7.5 关键点
popen
的返回值:- 返回一个
FILE*
指针,可以通过标准 I/O 函数读取或写入数据。
- 返回一个
pclose
的作用:- 关闭管道并等待外部应用程序结束。
- 返回外部应用程序的退出状态。
- 管道类型:
"r"
:读取外部应用程序的输出。"w"
:向外部应用程序输入数据。
- 错误处理:
- 如果
popen
或pclose
失败,会返回NULL
或-1
,并设置errno
。
- 如果
4.7.6 总结
popen
提供了一种简单的方式与外部应用程序通信。- 通过
popen
,可以读取外部应用程序的输出或向其输入数据。 pclose
用于关闭管道并获取外部应用程序的退出状态。popen
和pclose
是处理命令行工具输出的强大工具,适合需要与外部程序交互的场景。
4.8 常用的进程检查宏
在 Linux 系统中,进程的退出状态可以通过 wait
或 waitpid
函数获取。为了检查进程的退出状态,C 标准库提供了一组宏,用于解析进程的退出状态。这些宏定义在 <sys/wait.h>
头文件中。
以下是常用的进程检查宏及其作用:
4.9 常用进程检查宏
宏名 | 描述 |
---|---|
WIFEXITED(status) |
检查进程是否正常退出(通过 exit 或 _exit 退出)正常退出,返回非零值(true )。 |
WEXITSTATUS(status) |
如果进程正常退出,获取进程的退出状态(exit 或 _exit 的参数)。 |
WIFSIGNALED(status) |
检查进程是否因信号而终止。 |
WTERMSIG(status) |
如果进程因信号终止,获取导致进程终止的信号编号。 |
WIFSTOPPED(status) |
检查进程是否处于停止状态(例如,被 SIGSTOP 或 SIGTSTP 信号停止)。 |
WSTOPSIG(status) |
如果进程处于停止状态,获取导致进程停止的信号编号。 |
WIFCONTINUED(status) |
检查进程是否从停止状态恢复(收到 SIGCONT 信号)。 |
4.9.1 示例代码
以下代码演示了如何使用这些宏检查子进程的退出状态:
1 |
|
output:1
2Child process (PID = 1234)
Child exited normally with status: 42
4.9.2 总结
- 进程检查宏是解析进程退出状态的重要工具。
- 通过结合
wait
或waitpid
使用这些宏,可以获取进程的退出状态、终止信号等信息。 - 这些宏在编写多进程程序时非常有用,尤其是在需要处理子进程的退出状态时。
4.10 守护进程
守护进程(Daemon Process)是在后台运行的一种特殊进程,通常用于执行系统任务或服务,而不与任何终端或用户交互。守护进程在 Linux 系统中非常常见,例如网络服务(如 httpd
)、日志服务(如 syslogd
)等。
4.10.1 守护进程的特点
脱离终端:
- 守护进程不与任何终端关联,因此不会受到终端关闭的影响。
后台运行:
- 守护进程在后台运行,不会占用终端输入输出。
生命周期长:
- 守护进程通常从系统启动时开始运行,直到系统关闭。
无控制终端:
- 守护进程没有控制终端,因此不会接收来自终端的信号(如
SIGINT
、SIGHUP
等)。
- 守护进程没有控制终端,因此不会接收来自终端的信号(如
以 root 权限运行:
- 许多守护进程需要以 root 权限运行,以便访问系统资源或执行特权操作。
4.10.2 创建守护进程的步骤
以下是创建一个守护进程的标准步骤:
4.10.2.1 . 调用 fork
创建子进程
- 父进程退出,子进程继续运行。
- 这样可以使子进程脱离终端。
4.10.2.2 . 调用 setsid
创建新会话
- 子进程调用
setsid
创建一个新的会话,并成为该会话的组长。 - 这会使子进程脱离控制终端。
4.10.2.3 . 再次调用 fork
(可选)
- 再次调用
fork
创建孙子进程,并退出子进程。 - 这样可以确保孙子进程不是会话组长,从而无法重新获取控制终端。
- 会话由一个或多个进程组成,会话组组长的生命周期决定了会话的生命周期。
4.10.2.4 . 更改工作目录
- 将工作目录更改为根目录(
/
)或其他安全目录。 - 这样可以避免守护进程占用挂载的文件系统。
4.10.2.5 . 重设文件权限掩码
- 调用
umask(0)
重设文件权限掩码。 - 这样可以确保守护进程创建的文件具有预期的权限。
4.10.2.6 . 关闭文件描述符
- 关闭所有不需要的文件描述符(如标准输入、标准输出、标准错误)。
- 这样可以释放资源并避免干扰。
4.10.2.7 . 处理信号
- 忽略或处理某些信号(如
SIGHUP
、SIGTERM
等),以确保守护进程不会被意外终止。
4.10.3 示例代码
以下是一个简单的守护进程实现:
1 |
|
4.10.4 守护进程的管理
启动守护进程:
- 守护进程通常通过系统启动脚本(如
/etc/init.d
或systemd
服务)启动。
- 守护进程通常通过系统启动脚本(如
停止守护进程:
- 通过发送信号(如
SIGTERM
)停止守护进程。 - 例如:
kill <pid>
。
- 通过发送信号(如
查看守护进程:
- 使用
ps
命令查看守护进程:1
ps -ef | grep <daemon_name>
- 使用
日志记录:
- 守护进程通常将日志写入文件(如
/var/log
目录)。 - 可以使用
syslog
函数将日志发送到系统日志服务。
- 守护进程通常将日志写入文件(如
4.10.5 守护进程的日志记录
以下是一个使用 syslog
记录日志的示例:
1 |
|
4.10.6 总结
- 守护进程是后台运行的特殊进程,通常用于执行系统任务或服务。
- 创建守护进程的步骤包括:
fork
、setsid
、更改工作目录、重设文件权限掩码、关闭文件描述符等。 - 守护进程的管理包括启动、停止、查看和日志记录。
- 守护进程是 Linux 系统中实现后台服务的重要机制。
测验
写一个多进程 的程序, 用于拷贝文件
假设 一个文件为 100字节,在主进程中 计算 文件的大小 100,分两个进程 ,进程 A 拷贝 0-49,进程 B 拷贝 50-99。
1 |
|
output:
5 线程管理
5.1 线程的创建
新建线程pthread_create
1 | NAME |
- 参数说明
(1) pthread_t *thread
- 作用:用于存储新线程的标识符(线程 ID)。
- 类型:
pthread_t
是一个不透明的数据类型,用于唯一标识一个线程。 - 注意:线程 ID 由系统分配,创建成功后可以通过该 ID 操作线程。
(2)const pthread_attr_t *attr
- 作用:用于设置线程的属性(如栈大小、调度策略等)。
- 类型:
pthread_attr_t
是一个结构体,用于描述线程的属性。 - 注意:
- 如果传入
NULL
,则使用默认属性。 - 如果需要自定义属性,可以使用
pthread_attr_init
初始化属性对象,并通过pthread_attr_setxxx
系列函数设置属性。
- 如果传入
(3) void *(*start_routine) (void *)
- 作用:线程启动后执行的函数。
- 类型:函数指针,指向一个返回
void *
并接受void *
参数的函数。 - 注意:
- 该函数是线程的入口点,线程启动后会立即执行该函数。
- 函数的返回值可以通过
pthread_join
获取。
(4) void *arg
- 作用:传递给
start_routine
函数的参数。 - 类型:
void *
,可以指向任意类型的数据。 - 注意:
- 如果需要传递多个参数,可以将它们封装到一个结构体中,然后传递结构体的指针。
- 确保参数的生命周期在线程使用期间有效。
返回值
- 成功:返回
0
。 - 失败:返回错误码(非零值),常见的错误码包括:
EAGAIN
:系统资源不足,无法创建线程。EINVAL
:无效的线程属性。EPERM
:没有权限设置调度策略或参数。
等待线程pthread_join
结束线程pthread_exit
测验
多线程拷贝文件
1 |
|
output
1 | filelen:100 |
6 主要的线程间通信方法
线程间通信:
通信方式 | 描述 | 适用场景 |
---|---|---|
共享变量 | 通过全局变量或堆内存通信 | 简单的数据共享 |
互斥锁 | 保护共享资源 | 临界区保护 |
条件变量 | 线程间的条件同步 | 复杂的线程同步 |
信号量 | 控制对共享资源的访问 | 资源计数或互斥 |
屏障 | 同步多个线程 | 多线程协同工作 |
读写锁 | 允许多读单写 | 读多写少的场景 |
自旋锁 | 忙等待的锁 | 短时间的锁竞争 |
消息队列 | 通过消息队列传递数据 | 线程间数据传递 |
管道 | 通过管道传递数据 | 线程间数据传递 |
文件描述符 | 通过文件描述符通信 | 线程间数据传递 |
信号 | 通过信号通信 | 线程间通知 |
常用且较为基础的有 共享变量、互斥锁、条件变量、信号量(无名)、管道(无名)。
6.1 线程间互斥锁通信
在C语言中,线程间互斥锁(Mutex)是一种用于同步线程的机制,确保多个线程不会同时访问共享资源,从而避免竞态条件(Race Condition)。互斥锁的基本思想是,当一个线程需要访问共享资源时,它会先锁定互斥锁,其他线程在尝试锁定同一个互斥锁时会被阻塞,直到第一个线程释放锁。
6.1.1 . 互斥锁的基本操作
在C语言中,互斥锁通常通过 pthread
库来实现。以下是互斥锁的基本操作:
在使用互斥锁之前,需要先初始化它。可以使用 pthread_mutex_init
函数来初始化互斥锁。
1 |
|
或者使用 pthread_mutex_init
函数:
1 | pthread_mutex_t mutex; |
当一个线程需要访问共享资源时,可以使用 pthread_mutex_lock
函数来锁定互斥锁。如果互斥锁已经被其他线程锁定,当前线程会被阻塞,直到互斥锁被释放。
1 | pthread_mutex_lock(&mutex); |
当线程完成对共享资源的访问后,应该使用 pthread_mutex_unlock
函数来解锁互斥锁,以便其他线程可以访问共享资源。
1 | pthread_mutex_unlock(&mutex); |
当互斥锁不再需要时,应该使用 pthread_mutex_destroy
函数来销毁它,释放相关资源。
1 | pthread_mutex_destroy(&mutex); |
6.1.2 . 示例代码
以下是一个简单的示例,展示了如何使用互斥锁来同步两个线程对共享资源的访问。
1 |
|
6.1.3 . 注意事项
- 死锁:如果多个线程互相等待对方释放锁,可能会导致死锁。因此,在使用互斥锁时,要确保锁的获取和释放顺序一致。
- 性能:频繁的锁操作可能会影响程序的性能,因此应尽量减少锁的持有时间。
通过使用互斥锁,可以有效地避免多个线程同时访问共享资源导致的数据不一致问题。
6.1.4 如何避免死锁问题
- 尽量保持线程顺序进行,即保持同步。
- 避免互斥锁嵌套
6.2 线程间信号量(无名)通信(同步)
无名信号量(Anonymous Semaphore),也称为基于内存的信号量,是一种用于线程间或进程间同步的机制。与有名信号量不同,无名信号量没有关联的文件系统路径名,而是直接存储在内存中。它通常用于同一进程内的线程间同步,但也可以通过共享内存的方式用于进程间同步。
以下是关于无名信号量的详细介绍:
6.2.1 . 无名信号量的特点
- 无名称:无名信号量没有关联的文件系统路径名,因此只能通过内存地址访问。
- 高效:由于不需要文件系统操作,无名信号量的性能通常比有名信号量更高。
- 适用范围:
- 主要用于同一进程内的线程间同步。
- 如果需要在进程间使用,可以将无名信号量放置在共享内存中。
- 生命周期:无名信号量的生命周期与创建它的进程或线程绑定,进程退出后信号量会自动销毁。
6.2.2 . 无名信号量的操作函数
无名信号量的操作主要通过以下函数实现:
6.2.2.1 (1)初始化信号量
1 | int sem_init(sem_t *sem, int pshared, unsigned int value); |
- 功能:初始化一个无名信号量。
- 参数:
sem
:指向信号量对象的指针。pshared
:0
:信号量用于线程间同步。- 非零值:信号量用于进程间同步(需要将信号量放置在共享内存中)。
value
:信号量的初始值。
- 返回值:成功返回
0
,失败返回-1
。
6.2.2.2 (2)等待信号量(P 操作)
1 | int sem_wait(sem_t *sem); |
- 功能:等待信号量。如果信号量的值大于 0,则减 1 并继续执行;否则,线程阻塞。
- 参数:
sem
:指向信号量对象的指针。
- 返回值:成功返回
0
,失败返回-1
。
6.2.2.3 (3)释放信号量(V 操作)
1 | int sem_post(sem_t *sem); |
- 功能:释放信号量,将信号量的值加 1,并唤醒等待的线程。
- 参数:
sem
:指向信号量对象的指针。
- 返回值:成功返回
0
,失败返回-1
。
6.2.2.4 (4)销毁信号量
1 | int sem_destroy(sem_t *sem); |
- 功能:销毁一个无名信号量。
- 参数:
sem
:指向信号量对象的指针。
- 返回值:成功返回
0
,失败返回-1
。
6.2.3 . 无名信号量的使用场景
无名信号量通常用于以下场景:
- 线程间同步:
- 控制多个线程对共享资源的访问。
- 实现生产者-消费者模型。
- 进程间同步:
- 将无名信号量放置在共享内存中,供多个进程使用。
- 二进制信号量:
- 信号量的初始值为 1,用于实现互斥锁的功能。
6.2.4 . 无名信号量与有名信号量的区别
特性 | 无名信号量 | 有名信号量 |
---|---|---|
名称 | 无名称,通过内存地址访问 | 有名称,通过文件系统路径访问 |
适用范围 | 主要用于线程间同步 | 可用于线程间和进程间同步 |
性能 | 更高(无需文件系统操作) | 较低(涉及文件系统操作) |
生命周期 | 与创建它的进程或线程绑定 | 独立于进程,需手动销毁 |
初始化方式 | sem_init |
sem_open |
销毁方式 | sem_destroy |
sem_close 和 sem_unlink |
6.2.5 . 无名信号量的示例
以下是一个简单的示例,展示如何使用无名信号量实现线程间同步:
1 |
|
output:
1 | conut:1 |
无名信号量存在sem_wait(&sem);
阻塞等待的问题,所以在多个线程时使用互斥锁并与条件变量配合使用。
6.3 线程间条件变量通信(同步)
6.3.1 条件变量
条件变量(Condition Variable)是一种线程同步机制,通常与互斥锁(Mutex)结合使用,用于实现线程间的复杂同步。它的核心功能是允许线程在某个条件不满足时进入休眠状态,并在条件满足时被唤醒。条件变量是多线程编程中非常重要的工具,常用于实现生产者-消费者模型、线程池等场景。
6.3.2 条件变量的核心概念
- 等待条件:线程可以检查某个条件是否满足,如果不满足,则进入休眠状态。
- 通知条件:当条件满足时,其他线程可以唤醒等待的线程。
- 与互斥锁结合:条件变量通常与互斥锁一起使用,以确保对共享资源的访问是线程安全的。
6.3.3 条件变量的操作函数
条件变量的操作主要通过以下函数实现:
6.3.3.1 初始化条件变量
1 | int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); |
- 功能:初始化一个条件变量。
- 参数:
cond
:指向条件变量对象的指针。attr
:条件变量的属性,通常设置为NULL
,表示使用默认属性。
- 返回值:成功返回
0
,失败返回错误码。
6.3.3.2 销毁条件变量
1 | int pthread_cond_destroy(pthread_cond_t *cond); |
- 功能:销毁一个条件变量。
- 参数:
cond
:指向条件变量对象的指针。
- 返回值:成功返回
0
,失败返回错误码。
6.3.3.3 等待条件变量
1 | int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); |
- 功能:等待条件变量。线程会释放互斥锁并进入休眠状态,直到被唤醒。
- 参数:
cond
:指向条件变量对象的指针。mutex
:指向互斥锁对象的指针。
- 返回值:成功返回
0
,失败返回错误码。
6.3.3.4 唤醒一个等待的线程
1 | int pthread_cond_signal(pthread_cond_t *cond); |
- 功能:唤醒一个等待条件变量的线程。
- 参数:
cond
:指向条件变量对象的指针。
- 返回值:成功返回
0
,失败返回错误码。
6.3.3.5 唤醒所有等待的线程
1 | int pthread_cond_broadcast(pthread_cond_t *cond); |
- 功能:唤醒所有等待条件变量的线程。
- 参数:
cond
:指向条件变量对象的指针。
- 返回值:成功返回
0
,失败返回错误码。
6.3.3.6 条件变量的使用模式
条件变量通常与互斥锁和共享变量一起使用,以下是一个典型的使用模式:
1 | pthread_mutex_t mutex; |
6.3.3.7 条件变量的注意事项
虚假唤醒:
线程可能会在没有收到通知的情况下被唤醒,因此需要在
pthread_cond_wait
返回后重新检查条件。通常将
pthread_cond_wait
放在while
循环中:1
2
3while (condition == 0) {
pthread_cond_wait(&cond, &mutex);
}
与互斥锁的配合:
- 在调用
pthread_cond_wait
之前,必须持有互斥锁。 pthread_cond_wait
会释放互斥锁并进入休眠状态,被唤醒后会重新获取互斥锁。
- 在调用
避免死锁:
- 确保在修改条件和调用
pthread_cond_signal
或pthread_cond_broadcast
时持有互斥锁。 - 确保在等待条件变量时正确释放和重新获取互斥锁。
- 确保在修改条件和调用
6.3.4 示例代码
以下是一个完整的生产者-消费者模型的示例:
1 |
|
6.4 线程间管道(无名)通信
管道是一种半双工的通信机制,数据只能单向流动。它有两个端点:
- 写端:用于写入数据。
- 读端:用于读取数据。
在 Linux 中,管道通过 pipe()
系统调用创建,返回两个文件描述符:一个用于读,一个用于写。
6.4.1 线程间使用管道通信的步骤
以下是在同一进程内的线程间使用管道通信的基本步骤:
使用 pipe()
函数创建管道:1
2
3
4
5
6
7
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pipefd[0]
是读端文件描述符。pipefd[1]
是写端文件描述符。
使用 pthread_create()
创建线程,并将管道文件描述符传递给线程函数。
- 一个线程通过
write()
向管道写入数据。 - 另一个线程通过
read()
从管道读取数据。
通信完成后,使用 close()
关闭管道文件描述符。
6.4.2 测验
演示两个线程如何通过管道通信:
1 |
|
7 主要的进程间通信方法
进程间通信:
通信方式 | 描述 | 适用场景 |
---|---|---|
管道 | 半双工,字节流 | 父子进程间通信 |
命名管道 | 通过文件系统路径访问 | 无关进程间通信 |
消息队列 | 消息传递,支持消息边界 | 进程间结构化数据传递 |
共享内存 | 共享内存区域,速度快 | 高性能数据共享 |
信号量 | 进程间同步 | 资源计数或互斥 |
信号 | 异步通知 | 事件通知 |
套接字 | 支持本地和网络通信 | 跨机器或本地进程间通信 |
对于无名管道的操作与线程类似,但对于进程间通信无名方式只适用于有亲缘关系的进程。
7.1 命名管道
有名管道(Named Pipe),也称为 FIFO(First In First Out),是一种特殊的文件类型,用于实现进程间通信(IPC)。与匿名管道(Pipe)不同,有名管道可以在无关的进程之间使用,因为它通过文件系统中的路径名来标识。
7.1.1 . 有名管道的特点
- 基于文件系统:有名管道在文件系统中有一个路径名,多个进程可以通过该路径名访问管道。
- 半双工通信:数据只能单向流动,如果需要双向通信,需要创建两个有名管道。
- 阻塞与非阻塞:默认情况下,读操作和写操作是阻塞的,但可以通过设置非阻塞模式来改变行为。
- 持久性:有名管道在文件系统中存在,直到被显式删除。
7.1.2 . 有名管道的操作步骤
7.1.2.1 创建有名管道
使用 mkfifo
命令或 mkfifo
函数创建有名管道。
使用 mkfifo
命令:
1 | mkfifo /tmp/myfifo |
使用 mkfifo
函数:
1 |
|
pathname
:有名管道的路径名。mode
:管道的权限模式(如0666
)。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
if (mkfifo("/tmp/myfifo", 0666) == -1) {
perror("mkfifo");
exit(1);
}
printf("FIFO created at /tmp/myfifo\n");
return 0;
}
7.1.2.2 打开有名管道
使用 open
函数打开有名管道。打开时需要指定读写模式:
- 只读模式:
O_RDONLY
- 只写模式:
O_WRONLY
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
int fd = open("/tmp/myfifo", O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}
printf("FIFO opened for writing\n");
return 0;
}
7.1.2.3 读写有名管道
使用 read
和 write
函数进行读写操作。
写入数据:
1 |
|
读取数据:
1 |
|
7.1.2.4 关闭和删除有名管道
使用 close
函数关闭管道,使用 unlink
函数删除管道文件。
示例:1
2
3
unlink("/tmp/myfifo");
7.1.3 . 完整示例
以下是一个完整的示例,展示如何使用有名管道实现进程间通信。
7.1.3.1 写入端(writer.c
):
1 |
|
7.1.3.2 读取端(reader.c
):
1 |
|
7.1.4 . 注意事项
阻塞行为:
- 默认情况下,读操作会阻塞,直到有数据写入;写操作会阻塞,直到有进程读取数据。
- 可以通过设置非阻塞模式(
O_NONBLOCK
)来改变行为。
双向通信:
- 如果需要双向通信,可以创建两个有名管道,一个用于进程 A 到进程 B 的通信,另一个用于进程 B 到进程 A 的通信。
管道清理:
- 使用
unlink
删除管道文件,避免残留。
- 使用
7.2 信号
在操作系统中,进程间的信号(Signal)通信是一种异步的通信机制,用于通知进程发生了某种事件。信号可以由操作系统、其他进程或进程自身发送。进程可以捕获信号并执行相应的处理函数,也可以忽略信号或使用默认行为。
7.2.1 . 信号的基本概念
信号是什么:
信号是一个整数,表示某种事件的发生。例如:SIGINT
:中断信号(通常由Ctrl+C
触发)。SIGTERM
:终止信号(请求进程终止)。SIGKILL
:强制终止信号(无法被捕获或忽略)。SIGUSR1
和SIGUSR2
:用户自定义信号。
信号的来源:
- 由操作系统内核发送(如段错误
SIGSEGV
)。 - 由其他进程发送(通过
kill
系统调用)。 - 由进程自身发送(通过
raise
或kill
)。
- 由操作系统内核发送(如段错误
信号的处理方式:
- 捕获信号:进程可以注册一个信号处理函数来处理信号。
- 忽略信号:进程可以选择忽略某些信号。
- 默认行为:如果不捕获或忽略信号,进程会执行信号的默认行为(如终止、暂停等)。
常用的信号
| 信号名称 | 信号编号 | 类型 | 描述 |
| :———- | :———- | :—- | :———————————————————————— |
| SIGHUP | 1 | Term | 控制终端挂起或控制进程死亡。 |
| SIGINT | 2 | Term | 键盘中断(通常是Ctrl+C
)。 |
| SIGQUIT | 3 | Core | 键盘退出(通常是Ctrl+\
),并生成核心转储文件。 |
| SIGILL | 4 | Core | 非法指令。 |
| SIGABRT | 6 | Core | 由abort(3)
函数发出的中止信号。 |
| SIGFPE | 8 | Core | 浮点异常。 |
| SIGKILL | 9 | Term | 强制终止信号(无法被捕获或忽略)。 |
| SIGSEGV | 11 | Core | 无效内存引用(段错误)。 |
| SIGPIPE | 13 | Term | 管道破裂:向没有读取者的管道写入数据。 |
| SIGALRM | 14 | Term | 由alarm(2)
函数设置的定时器信号。 |
| SIGTERM | 15 | Term | 终止信号(请求进程正常终止)。 |
| SIGUSR1 | 30,10,16 | Term | 用户自定义信号 1。 |
| SIGUSR2 | 31,12,17 | Term | 用户自定义信号 2。 |
| SIGCHLD | 20,17,18 | Ign | 子进程停止或终止。 |
| SIGCONT | 19,18,25 | Cont | 如果进程已停止,则继续执行。 |
| SIGSTOP | 17,19,23 | Stop | 停止进程(无法被捕获或忽略)。 |
| SIGTSTP | 18,20,24 | Stop | 终端发出的停止信号(通常是Ctrl+Z
)。 |
| SIGTTIN | 21,21,26 | Stop | 后台进程尝试从终端读取输入。 |
| SIGTTOU | 22,22,27 | Stop | 后台进程尝试向终端写入输出。 |- 类型:
- Term:终止进程。
- Core:终止进程并生成核心转储文件。
- Ign:忽略信号。
- Cont:继续已停止的进程。
- Stop:停止进程。
- 信号编号:不同系统可能使用不同的编号,因此列出了多个可能的编号。
7.2.2 . 信号的发送与接收
7.2.2.1 发送信号
可以使用 kill
系统调用向指定进程发送信号。
1 |
|
pid
:目标进程的进程 ID。- 如果
pid > 0
,信号发送给指定进程。 - 如果
pid == 0
,信号发送给与当前进程同组的所有进程。 - 如果
pid == -1
,信号发送给所有有权限发送的进程。
- 如果
sig
:要发送的信号(如SIGUSR1
)。
示例:1
kill(1234, SIGUSR1); // 向进程 ID 为 1234 的进程发送 SIGUSR1 信号
7.2.2.2 捕获信号
可以使用 signal
或 sigaction
函数来注册信号处理函数。
signal
函数:1
2
3
void (*signal(int sig, void (*handler)(int)))(int);sig
:要捕获的信号。handler
:信号处理函数。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void handle_signal(int sig) {
printf("Received signal: %d\n", sig);
}
int main() {
signal(SIGUSR1, handle_signal); // 捕获 SIGUSR1 信号
while (1) {
sleep(1); // 等待信号
}
return 0;
}
示例2:
1 | int flag = 1; |
sigaction
函数:sigaction
是更强大的信号处理函数,可以设置更多的选项。1
2
3
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);sig
:要捕获的信号。act
:新的信号处理配置。oldact
:旧的信号处理配置(可以为NULL
)。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void handle_signal(int sig) {
printf("Received signal: %d\n", sig);
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL); // 捕获 SIGUSR1 信号
while (1) {
sleep(1); // 等待信号
}
return 0;
}
7.2.3 . 信号的默认行为
如果不捕获信号,进程会执行信号的默认行为。常见的默认行为包括:
- 终止进程:如
SIGTERM
、SIGINT
。 - 忽略信号:如
SIGCHLD
。 - 暂停进程:如
SIGSTOP
。 - 终止并生成核心转储文件:如
SIGSEGV
。
7.2.4 . 信号的阻塞与解除阻塞
进程可以阻塞某些信号,使其暂时不被处理。可以使用 sigprocmask
函数来设置信号掩码。
1 |
|
how
:操作类型,如SIG_BLOCK
(阻塞信号)、SIG_UNBLOCK
(解除阻塞)、SIG_SETMASK
(设置新的信号掩码)。set
:要操作的信号集。oldset
:旧的信号集(可以为NULL
)。
示例:1
2
3
4sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1); // 将 SIGUSR1 添加到信号集
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞 SIGUSR1
7.2.5 . 示例:进程间信号通信
以下是一个简单的示例,展示了一个进程向另一个进程发送信号并捕获信号的过程。
1 |
|
7.2.6 . 注意事项
- 信号是异步的:信号可以在任何时候中断进程的执行。
- 信号处理函数应尽量简单:避免在信号处理函数中执行复杂的操作。
- 信号可能丢失:如果多个相同的信号在短时间内发送,进程可能只会收到一个信号。
- 不可靠信号与可靠信号:早期的 UNIX 信号机制是不可靠的,现代系统(如 Linux)提供了可靠的信号机制(如
sigaction
)。
7.3 IPC与IPCS命令
IPC 是操作系统提供的一种机制,用于在不同进程之间传递数据或同步操作。
- 同步通信(Synchronous Communication)
同步通信是指通信的双方(发送者和接收者)必须协调时序,发送者发送数据后,必须等待接收者接收并处理数据,才能继续执行后续操作。
- 异步通信(Asynchronous Communication)
异步通信是指通信的双方不需要协调时序,发送者发送数据后,可以立即继续执行后续操作,而不需要等待接收者接收数据。
7.3.1 ipcs
命令
ipcs
是一个用于查看系统中 IPC 资源状态的命令行工具。它可以显示当前系统中使用的消息队列、共享内存段和信号量集的信息。
7.3.1.1 . 命令格式
1 | ipcs |
7.3.1.2 . 常用选项
选项 | 描述 |
---|---|
-a |
显示所有 IPC 资源(默认选项)。 |
-q |
仅显示消息队列(Message Queue)信息。 |
-m |
仅显示共享内存(Shared Memory)信息。 |
-s |
仅显示信号量(Semaphore)信息。 |
-l |
显示 IPC 资源的系统限制(如最大消息队列数、最大共享内存大小等)。 |
-p |
显示与 IPC 资源相关的进程 ID(创建者和最后操作者)。 |
-t |
显示 IPC 资源的访问时间(最后操作时间)。 |
-c |
显示 IPC 资源的创建者和所有者的用户 ID 和组 ID。 |
-u |
显示 IPC 资源的使用情况摘要。 |
7.3.1.3 . 输出字段说明
消息队列(Message Queue):
msqid
:消息队列的唯一标识符。owner
:消息队列的所有者。perms
:权限(访问控制)。used-bytes
:消息队列中已使用的字节数。messages
:消息队列中的消息数量。
共享内存(Shared Memory):
shmid
:共享内存段的唯一标识符。owner
:共享内存段的所有者。perms
:权限(访问控制)。bytes
:共享内存段的大小。nattch
:附加到共享内存段的进程数量。
信号量(Semaphore):
semid
:信号量集的唯一标识符。owner
:信号量集的所有者。perms
:权限(访问控制)。nsems
:信号量集中的信号量数量。
```
——— Message Queues ————
key msqid owner perms used-bytes messages
0x41021208 0 serenitati 666 0 0——— Shared Memory Segments ————
key shmid owner perms bytes nattch status——— Semaphore Arrays ————
key semid owner perms nsems1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#### `ipcrm` 命令
`ipcrm` 是用于删除 IPC 资源的命令。可以通过指定 IPC 资源的 ID 或键值来删除消息队列、共享内存段或信号量集。
#### 2. 常用选项
| 选项 | 描述 |
| :--- | :----------------------- |
| `-q` | 删除消息队列。 |
| `-m` | 删除共享内存段。 |
| `-s` | 删除信号量集。 |
| `-Q` | 通过键值删除消息队列。 |
| `-M` | 通过键值删除共享内存段。 |
| `-S` | 通过键值删除信号量集。 |
### 消息队列
消息队列(Message Queue)是一种进程间通信(IPC)机制,允许不同进程通过发送和接收消息来交换数据。消息队列的核心思想是:消息的发送者和接收者通过一个共享的队列进行通信,发送者将消息放入队列,接收者从队列中取出消息。
消息队列的特点是:
- **异步通信**:发送者和接收者不需要同时运行。
- **消息边界**:消息是独立的数据单元,接收者可以一次读取一条完整的消息。
- **优先级支持**:可以为消息设置优先级,高优先级的消息先被处理。
---
#### 1. 消息队列的基本概念
- **消息**:消息队列中的数据单元,通常是一个结构体,包含消息类型和消息内容。
- **消息队列标识符**:每个消息队列都有一个唯一的标识符(`msqid`),用于标识消息队列。
- **消息类型**:每条消息都有一个类型字段,接收者可以根据类型选择性地读取消息。
---
#### 2. 消息队列的操作函数
在 Linux 中,消息队列的操作主要通过以下系统调用实现:
| 函数 | 描述 |
| -------- | ---------------------------------------------------- |
| `msgget` | 创建或获取一个消息队列。 |
| `msgsnd` | 向消息队列发送一条消息。 |
| `msgrcv` | 从消息队列接收一条消息。 |
| `msgctl` | 控制消息队列(如删除消息队列、获取消息队列信息等)。 |
---
#### 3. 消息队列的使用步骤
##### 3.1 创建或获取消息队列
使用 `msgget` 函数创建或获取一个消息队列。
```c
#include <sys/msg.h>
int msgget(key_t key, int msgflg);key
:消息队列的键值,通常使用ftok
函数生成。msgflg
:标志位,如IPC_CREAT
(创建消息队列)和权限模式(如0666
)。
示例:
1 | key_t key = ftok("/tmp", 'A'); // 生成键值 |
3.2 发送消息
使用 msgsnd
函数向消息队列发送消息。
1 |
|
msqid
:消息队列标识符。msgp
:指向消息的指针,消息结构体必须包含一个long
类型的消息类型字段。msgsz
:消息的大小(不包括消息类型字段)。msgflg
:标志位,如IPC_NOWAIT
(非阻塞发送)。
消息结构体示例:
1 | struct msgbuf { |
发送消息示例:
1 | struct msgbuf msg; |
3.3 接收消息
使用 msgrcv
函数从消息队列接收消息。
1 |
|
msqid
:消息队列标识符。msgp
:指向消息的指针。msgsz
:消息缓冲区的大小。msgtyp
:消息类型:0
:接收队列中的第一条消息。> 0
:接收指定类型的消息。< 0
:接收类型小于或等于|msgtyp|
的第一条消息。
msgflg
:标志位,如IPC_NOWAIT
(非阻塞接收)。
接收消息示例:
1 | struct msgbuf msg; |
3.4 控制消息队列
使用 msgctl
函数控制消息队列。
1 |
|
msqid
:消息队列标识符。cmd
:控制命令,如IPC_RMID
(删除消息队列)。buf
:指向msqid_ds
结构的指针,用于获取或设置消息队列信息。
删除消息队列示例:
1 | msgctl(msqid, IPC_RMID, NULL); // 删除消息队列 |
4. 完整示例
以下是一个完整的示例,展示如何使用消息队列进行进程间通信。
发送者程序(sender.c
):
1 |
|
接收者程序(receiver.c
):
1 |
|
编译并运行:
1 | gcc -o sender sender.c |
共享内存
共享内存(Shared Memory)是一种高效的进程间通信(IPC)机制,允许多个进程共享同一块内存区域。由于数据直接存储在内存中,共享内存的通信速度比其他 IPC 机制(如管道、消息队列)更快。然而,共享内存本身不提供同步机制,因此通常需要结合信号量或互斥锁来避免竞态条件。
1. 共享内存的特点
- 高效:数据直接存储在内存中,无需内核介入。
- 无同步机制:共享内存本身不提供进程间的同步,需要额外的同步机制(如信号量)。
- 共享性:多个进程可以同时访问同一块内存区域。
- 持久性:共享内存段会一直存在,直到显式删除或系统重启。
2. 共享内存的操作步骤
2.1 创建共享内存段
使用 shmget
函数创建共享内存段。
1 |
|
key
:共享内存段的键值,通常使用ftok
生成。size
:共享内存段的大小(字节)。shmflg
:标志位,如IPC_CREAT
(创建共享内存段)和权限模式(如0666
)。
示例:1
2key_t key = ftok("/tmp", 'A');
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
2.2 附加共享内存段
使用 shmat
函数将共享内存段附加到进程的地址空间。
1 |
|
shmid
:共享内存段的标识符。shmaddr
:指定附加地址(通常为NULL
,由系统选择)。shmflg
:标志位,如SHM_RDONLY
(只读模式)。
示例:1
char *shmaddr = (char *)shmat(shmid, NULL, 0);
2.3 使用共享内存
进程可以通过附加的共享内存地址直接读写数据。
示例:1
strcpy(shmaddr, "Hello, Shared Memory!");
2.4 分离共享内存段
使用 shmdt
函数将共享内存段从进程的地址空间分离。
1 |
|
shmaddr
:共享内存段的附加地址。
示例:1
shmdt(shmaddr);
2.5 删除共享内存段
使用 shmctl
函数删除共享内存段。
1 |
|
shmid
:共享内存段的标识符。cmd
:控制命令,如IPC_RMID
(删除共享内存段)。buf
:指向shmid_ds
结构的指针(可以为NULL
)。
示例:1
shmctl(shmid, IPC_RMID, NULL);
3. 完整示例
以下是一个完整的示例,展示如何使用共享内存实现进程间通信。
写入端(writer.c
):
1 |
|
读取端(reader.c
):
1 |
|
有名型号量
有名信号量(Named Semaphore)是一种用于进程间同步的机制,它通过文件系统中的名字来标识信号量,因此可以用于无关进程间的同步。有名信号量的生命周期独立于创建它的进程,直到显式删除。
有名信号量与无名信号量间的区别
作用范围:
特性 | 无名信号量 | 有名信号量 |
---|---|---|
作用范围 | 线程间或父子进程间 | 无关进程间 |
共享方式 | 基于内存,通常位于共享内存中 | 基于文件系统,通过名字标识 |
生命周期:
特性 | 无名信号量 | 有名信号量 |
---|---|---|
生命周期 | 与创建它的进程或线程绑定 | 独立于创建它的进程 |
持久性 | 无持久性,进程退出后信号量被销毁 | 有持久性,信号量会一直存在,直到显式删除 |
使用场景:
特性 | 无名信号量 | 有名信号量 |
---|---|---|
适用场景 | 线程间同步、父子进程间同步 | 无关进程间同步 |
实现复杂度 | 较简单 | 较复杂 |
创建和销毁方式:
特性 | 无名信号量 | 有名信号量 |
---|---|---|
创建方式 | 使用 sem_init 初始化 |
使用 sem_open 创建或打开 |
销毁方式 | 使用 sem_destroy 销毁 |
使用 sem_close 关闭和 sem_unlink 删除 |
1. 有名信号量的特点
- 基于文件系统:有名信号量通过文件系统中的名字标识,可以在无关进程间共享。
- 持久性:有名信号量会一直存在,直到显式删除(使用
sem_unlink
)。 - 同步机制:有名信号量提供了一种简单的同步机制,可以用于控制多个进程对共享资源的访问。
2. 有名信号量的操作函数
有名信号量的操作函数定义在 <semaphore.h>
头文件中,主要包括以下函数:
函数 | 描述 |
---|---|
sem_open |
创建或打开一个有名信号量。 |
sem_wait |
等待信号量(信号量值减 1)。 |
sem_post |
释放信号量(信号量值加 1)。 |
sem_trywait |
非阻塞等待信号量。 |
sem_timedwait |
带超时的等待信号量。 |
sem_close |
关闭有名信号量。 |
sem_unlink |
删除有名信号量。 |
3. 有名信号量的使用步骤
3.1 创建或打开有名信号量
使用 sem_open
函数创建或打开一个有名信号量。
1 |
|
name
:信号量的名字,通常以/
开头(如/mysem
)。oflag
:标志位,如O_CREAT
(创建信号量)和O_EXCL
(如果信号量已存在则失败)。mode
:权限模式(如0666
)。value
:信号量的初始值。
示例:1
2
3
4
5sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}
3.2 等待信号量
使用 sem_wait
函数等待信号量(信号量值减 1)。
1 |
|
sem
:指向信号量的指针。
示例:1
sem_wait(sem); // 等待信号量
3.3 释放信号量
使用 sem_post
函数释放信号量(信号量值加 1)。
1 |
|
sem
:指向信号量的指针。
示例:1
sem_post(sem); // 释放信号量
3.4 关闭有名信号量
使用 sem_close
函数关闭有名信号量。
1 |
|
sem
:指向信号量的指针。
示例:1
sem_close(sem); // 关闭信号量
3.5 删除有名信号量
使用 sem_unlink
函数删除有名信号量。
1 |
|
name
:信号量的名字。
示例:1
sem_unlink("/mysem"); // 删除信号量
4. 完整示例
以下是一个完整的示例,展示如何使用有名信号量实现进程间同步。
写入端(writer.c
):
1 |
|
读取端(reader.c
):
1 |
|