进程的概念及基础
程序和进程的关系
程序:是静态的,一堆没有被加载(运行)的二进制代码,存储在硬盘中
进程:是动态的,已经被加载(运行)到内存中的程序文件,存储到内存中
进程的相关命令
进程的生命周期状态(重点)
1、就绪态:当一个程序被加载后就处于就绪态中,所有的进程要变成执行态,都要从就绪态开始
2、执行态:当一个进程得到 CPU 的使用权(时间片)后,就会处于执行态中
3、睡眠/挂起态:当一个进程调用 sleep 或者一些等待阻塞函数 scanf 时,都会进入到睡眠/挂起态
4、暂停态:收到 STOP 暂停信号后就会进入到暂停态中,等待 CONT 继续信号
5、僵尸态:进程死亡后都会进入到僵尸态,等待别人帮他收尸
6、死亡态:父母进程回收(wait)子进程的资源就会进入死亡态
进程的相关 API 和说明
进程的创建(fork 函数)
父子进程在 fork() 之后的区别,核心就一条:
👉 父进程和子进程的虚拟内存内容相同,但属于不同的进程,PID 不同,资源不共享(文件描述符除外)
具体来说:
- 不同点
- PID 不同:
getpid()获取到的进程号不同。 - fork() 的返回值不同:
- 父进程中
fork()返回子进程 PID。 - 子进程中
fork()返回0。
- 父进程中
- 资源独立:全局变量、局部变量等虽然初始值相同,但各自独立修改,互不影响
- PID 不同:
- 相同点
- 代码段相同:执行同一段程序。
- 数据段、堆、栈初始内容相同(fork 后复制)
- 打开的文件描述符相同(共享文件偏移量)
父子进程共享同一份程序代码,调度由操作系统时间片复用完成,所以运行起来像同一个程序
它们的区别只能通过 fork() 的返回值 和 进程号(PID) 来区分
1 |
|
运行结果如下:
1 | ➜ ./1.fork函数创建进程 |
可以看到,父进程和字进程变量的虚拟内存地址相同,但值不同
进程的回收(wait, waitpid, 状态值)
当一个子进程退出时,它会进入僵尸态。此时,子进程的资源(如内存)大部分已被释放,但在内核的进程表中仍然保留其退出状态等信息。父进程需要调用 wait() 或 waitpid() 来获取这些信息,并彻底清理子进程的记录,这个过程称为“为子进程收尸”。
1. wait() 函数
wait() 函数会阻塞父进程,直到它的任意一个子进程结束。
- 函数原型:
pid_t wait(int *wstatus); - 参数:
wstatus是一个整型指针,用于接收子进程的退出状态。如果不需要,可以传入NULL。 - 返回值:成功时返回结束的子进程的 PID;失败时返回 -1。
下面的代码演示了父进程如何使用 wait() 等待子进程结束。子进程运行 5 秒后退出,父进程则一直阻塞等待
1 |
|
2. waitpid() 函数
waitpid() 是 wait() 的一个更强大、更灵活的版本。它可以等待一个特定的子进程,也可以设置为非阻塞模式。
- 函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options); - 参数:
pid:> 0: 等待进程 ID 为pid的子进程。-1: 等待任意子进程(等同于wait())。
wstatus:同wait(),用于存储退出状态。options:0: 阻塞等待。WNOHANG: 非阻塞,如果子进程还未结束,立即返回0。WUNTRACED: 如果子进程进入暂停状态,也返回。
3. 获取子进程退出状态
当 wait() 或 waitpid() 返回后,wstatus 中包含了子进程的退出信息,不能直接当作退出码使用。
需要用一组宏来解析:
WIFEXITED(wstatus): 判断子进程是否正常退出(通过return,exit,_exit)。WEXITSTATUS(wstatus): 如果正常退出,用此宏获取退出状态码。
WIFSIGNALED(wstatus): 判断子进程是否被信号终止。WTERMSIG(wstatus): 如果是,用此宏获取终止它的信号编号。
WIFSTOPPED(wstatus): 判断子进程是否处于暂停状态。WSTOPSIG(wstatus): 如果是,用此宏获取使它暂停的信号编号。
以下代码演示了如何使用 waitpid() 和状态宏来详细判断子进程的退出原因
1 |
|
进程的退出(return, exit, _Exit)
进程的退出分为正常退出和异常退出。这里主要讨论三种正常退出的方式。
return- 在
main函数中使用return,等同于调用exit(return_value)。进程会正常终止,并执行清理操作。 - 在普通函数中使用
return,仅仅是将控制权返回给调用者,并不会导致进程退出。
- 在
exit()函数- 函数原型:
void exit(int status); - 这是标准的退出函数。无论在哪个函数中调用,都会立即终止整个进程。
- 特点:在进程退出前,会执行一系列“清理操作”,包括:
- 刷新标准 I/O 缓冲区(如
printf的内容)。 - 调用
atexit()注册的函数。
- 刷新标准 I/O 缓冲区(如
- 函数原型:
_exit()和_Exit()函数- 函数原型:
void _exit(int status); - 这两个函数功能相同,会立即终止进程,但不会执行任何清理操作。缓冲区的数据会丢失,
atexit注册的函数也不会被调用。 - 推荐场景:在
fork()后的子进程中,推荐使用_exit()。因为exit()的缓冲区刷新可能会影响父进程的 I/O 流,导致数据重复输出或混乱。使用_exit()可以避免这种副作用。
- 函数原型:
下面的代码演示了在普通函数 PTask1_PlayGame 中,三种退出方式的示例代码
1 |
|
多进程的使用
在实际应用中,可以通过创建多个子进程来并行处理不同的任务,以提高程序的效率和模块化程度。例如,一个车载系统可以为每个功能(如胎压监测、车速显示、油耗计算等)创建一个独立的进程。
- 实现思路:
- 父进程在一个循环中多次调用
fork()来创建所需数量的子进程。 - 在子进程的代码块中,通过循环变量或其他标识来区分不同的子进程,并让它们执行各自的任务函数。
- 每个子进程任务完成后,应调用
_exit()退出,避免不必要的资源清理副作用。 - 父进程在创建完所有子进程后,进入另一个循环,调用
wait()来回收所有子进程的资源,防止它们变成僵尸进程。
- 父进程在一个循环中多次调用
1 |
|
程序的加载(system, popen, exec 系列函数)
有时候,一个进程需要执行一个全新的程序。这可以通过加载外部程序来实现
1. system() 函数
system() 函数非常方便,它可以直接执行一个 shell 命令字符串。
- 函数原型:
int system(const char *command); - 工作原理:
system()会创建一个子进程来执行sh -c "command",并阻塞等待该命令执行完毕。 - 优点:使用简单,可以执行复杂的 shell 命令,如管道、重定向。
- 缺点:开销较大(创建了 shell 进程),且存在安全风险(命令注入)。
1 |
|
2. popen() 和 pclose() 函数
popen() 函数用于执行一个命令,但与 system() 不同的是,它会创建一个管道(pipe),使得当前进程可以读取该命令的输出,或者写入数据给该命令的输入。
- 函数原型:
FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
- 参数
type:"r": 读取模式。可以像读文件一样,读取command的标准输出。"w": 写入模式。可以像写文件一样,将数据写入到command的标准输入。
- pclose():必须调用
pclose()来关闭popen创建的流,它会等待命令执行结束。
1 |
|
3. exec 系列函数
exec 系列函数是最核心的程序加载机制。它不会创建新进程,而是用一个全新的程序替换当前进程的内存空间(包括代码、数据、堆栈)。
- 核心特点:一旦
exec调用成功,原来的程序就不存在了,PID 保持不变,但执行的已经是新程序了。exec成功后不会返回。 - 常见模式:
fork()+exec()。父进程fork一个子进程,然后子进程调用exec去执行新程序,父进程可以继续做自己的事或wait子进程。system和popen内部都封装了这个模式。 - 函数家族:
execl,execlp,execle: 参数以列表形式给出 (l)。(list)execv,execvp,execve: 参数以字符串数组形式给出 (v)。(vector)- 带
p的 (execlp,execvp) 会在PATH环境变量中搜索可执行文件。(path) - 带
e的 (execle,execve) 允许你自定义新程序的环境变量。(environment)
1 | // 简单的 fork + exec 示例 |
进程的关系(重点)
(1) 父子进程
通过 fork() 创建的进程具有父子关系。父进程是创建者,子进程是被创建者。父进程有责任管理其子进程的生命周期,最重要的是在子进程结束后,通过调用 wait() 或 waitpid() 来回收其资源,防止子进程变成僵尸进程。
可以使用 pstree 命令查看系统中所有进程形成的“家族树”,直观地看到父子关系。top 或 ps 命令也可以查看每个进程的 PID (进程 ID) 和 PPID (父进程 ID)。
1 |
|
(2) 孤儿进程
- 说明:当一个父进程比它的子进程先结束时,这个子进程就成为了“孤儿进程”。
- 系统处理:孤儿进程不会无人管理,它会被系统的 1 号进程(在现代 Linux 系统中通常是
systemd)所“收养”,systemd会成为它的新父进程,并负责在其结束后回收资源。 - 注意:虽然系统会自动处理孤儿进程,但这通常意味着程序设计上存在逻辑问题。正常情况下,父进程应该等待所有子进程结束后再退出。
1 |
|
运行现象:程序运行后,子进程会持续打印其父进程的 ID。5 秒后,父进程退出,你会观察到子进程打印的父进程 ID(PPID)从原来的父进程 PID 变成了 1
(3) 僵尸进程
- 说明:当一个子进程比父进程先结束,但父进程没有调用
wait()或waitpid()来获取子进程的退出状态时,子进程就会变成“僵尸进程”。 - 状态:此时,子进程的绝大部分资源(如内存)已被释放,但在内核的进程表中仍然保留着它的条目(包含 PID、退出状态、资源使用信息等),等待父进程来读取。在
ps命令中,僵尸进程的状态显示为Z(Zombie) 或<defunct>。 - 危害:僵尸进程本身不占用太多资源,但它会一直占用一个 PID。如果一个程序持续产生僵尸进程而不进行回收,最终会耗尽系统可用的 PID,导致无法创建任何新进程,从而使系统瘫痪。必须避免僵尸进程的产生
1 |
|
运行现象:运行该程序后,立即在另一个终端执行 ps aux | grep a.out,你会看到多个状态为 Z+ 的僵尸进程
(4) 守护进程 (精灵进程)
- 说明:守护进程是一种特殊的后台进程,它完全脱离于控制终端,独立于任何登录会话,通常用于执行周期性任务或提供系统服务(如
sshd,httpd等)。 - 特点:
- 长期运行:通常在系统启动时开始运行,直到系统关闭。
- 无控制终端:不与任何终端关联,不会接收终端输入或向终端输出。
- 会话独立:自己是一个独立的会话首进程。
- 后台运行:在系统后台默默执行任务。
- 特定工作目录:通常将工作目录切换到根目录 (
/),以防其所在的文件系统被卸载。 - 特定文件权限掩码:通常将 umask 设置为 0,以便对创建的文件拥有完全控制权。
- 创建步骤:
fork()创建子进程,父进程退出。- 子进程调用
setsid()创建新会话,使自己成为会话首进程、进程组组长,并脱离控制终端。 - 调用
chdir("/")改变工作目录到根目录。 - 调用
umask(0)重设文件权限掩码。 - 关闭不再需要的文件描述符(特别是标准输入、输出、错误)。
- 简化函数:
<unistd.h>提供了daemon()函数来简化上述步骤。int daemon(int nochdir, int noclose);nochdir: 若为 0,则改变目录到/。noclose: 若为 0,则将标准输入、输出、错误重定向到/dev/null。- 成功返回 0,失败返回-1。
1 |
|
运行现象:编译运行后,程序会立即返回,你看不到任何输出。但你可以通过 ps aux | grep a.out 找到这个进程在后台运行,并且 /tmp/daemon_time.log 文件会每秒被写入一次当前时间
其他的进程相关函数说明
进程创建(vfork 函数)
vfork() 是一个历史遗留的函数,其设计初衷是在 fork() 之后立即调用 exec() 的场景下提供更高的效率。由于现代 Linux 内核对 fork() 实现了“写时复制(Copy-On-Write)”优化,vfork() 的性能优势已不再明显,且其独特的行为容易导致错误,因此不推荐使用。
vfork() 与 fork() 的核心区别:
内存复制方式:
fork():子进程会获得父进程数据段、堆、栈的独立副本(通过写时复制技术实现)。vfork():子进程与父进程共享数据段。子进程对共享内存(如全局变量)的修改会直接影响到父进程。
执行顺序:
fork():父子进程的执行顺序由操作系统调度决定,是不确定的。vfork():保证子进程先运行,父进程会被阻塞,直到子进程调用了exec()系列函数或_exit()退出。
使用注意:
- 因为共享地址空间,在
vfork()创建的子进程中,不能从函数return或调用exit(),这会破坏父进程的栈帧。必须使用_exit()或exec()族函数来结束子进程
1 |
|
运行结果:
1 | --- 子进程开始执行 --- |
结果清晰地表明:子进程先执行,并且它对 g_num 的修改反映到了父进程中
进程间的通信
在现代操作系统中,为了安全和稳定,每个进程都拥有自己独立的虚拟内存空间。这意味着一个进程无法直接访问另一个进程的内存,这种机制被称为进程隔离。
它保证了一个进程的崩溃不会影响到其他进程,但同时也带来了挑战:进程之间如何协作和交换数据?
很多复杂应用,如 Web 服务器处理用户请求、数据库系统管理并发访问,都需要多个进程协同工作。为了解决进程隔离下的协作问题,操作系统内核提供了一系列官方的、安全可控的“沟通渠道”,即进程间通信(Inter-Process Communication, IPC) 机制。
这些机制本质上都是在内核空间开辟一块共享区域,允许不同进程通过这块区域进行数据交换或同步,如下图所示:
有哪些通信方式(重点):
Linux 系统提供了多种 IPC 方式,各有其特点和适用场景:
- (1) 文件和文件锁 (File Locking): 通过读写共享文件进行通信,使用文件锁同步,速度慢。
- (2) 管道 (Pipe): 包括只能用于亲缘进程的匿名管道和可用于任意进程的命名管道 (FIFO)。
- (3) 信号 (Signal): 用于异步通知,不传输数据,类似于系统中断。
- (4) 消息队列 (Message Queue): 内核维护的消息链表,适用于结构化、小规模数据通信。 (System V IPC)
- (5) 共享内存 (Shared Memory): 最快的 IPC 方式,将同一块物理内存映射到多个进程,适用于大规模数据交换。 (System V IPC)
- (6) 信号量 (Semaphore): 主要用于进程/线程间的同步与互斥,保护共享资源(如共享内存)。 (System V IPC)
- (7) 套接字 (Socket): 最通用的 IPC 机制,既可用于本机进程间通信,也可用于网络间通信
(1) 文件与文件锁
这是最直观的 IPC 方式。一个进程将数据写入文件,另一个进程从文件中读取。为防止并发读写造成数据损坏,必须使用文件锁。
- 原理:利用文件系统作为公共存储区。
- 同步:
flock()函数提供建议性锁。LOCK_EX(独占锁/写锁)保证只有一个进程可以写入,LOCK_SH(共享锁/读锁)允许多个进程同时读取。
1. flock 函数
- 头文件:
#include <sys/file.h> - 原型:
int flock(int fd, int operation); operation:LOCK_SH: 施加共享锁LOCK_EX: 施加独占锁LOCK_UN: 释放锁LOCK_NB: 与以上选项组合,使调用非阻塞
2. 示例代码:写进程
1 | // writer.c |
3. 示例代码:读进程
1 | // reader.c |
(2)、通过管道 (Pipe) 来通信
管道是 Linux 中一种基于文件的 IPC 机制,它在内核中创建一块缓冲区,用于进程间的单向数据传输,遵循“先进先出”(FIFO)原则。
A、匿名管道 (Anonymous Pipe)
1、说明
- 用途:只能用于具有亲缘关系的进程之间(通常是父子进程)。
- 特点:它没有文件系统中的路径名,存在于内存中,随进程的结束而消失。
- 核心:必须在
fork()之前调用pipe()创建管道,子进程会继承父进程的文件描述符,从而实现共享。通信时,父子进程通常会各自关闭管道的一个端口(一个关读,一个关写)。
2、图解
3、pipe() 函数的使用
使用命令:man 2 pipe 查看接口函数。
1 | // 函数头文件: |
4、示例代码:父子进程通信
1 | /** |
B、命名管道 (Named Pipe / FIFO)
1、说明
- 用途:可以在任意两个不相关的进程之间进行通信。
- 特点:它在文件系统中以一个特殊文件的形式存在(文件类型为
p),有具体的路径名。只要进程有权限访问这个文件,就可以进行通信。 - 阻塞特性:打开命名管道时,读端会阻塞直到写端被打开,反之亦然。
2、创建命令与函数
- 终端命令:
mkfifo <管道文件名>,例如mkfifo /tmp/myfifo。 - mkfifo() 函数:
man 3 mkfifo查看接口函数。
1 | // 函数头文件: |
3、示例代码:不相关进程通信
a、写入命名管道 (writer.c)
1 | /** |
b、读取命名管道 (reader.c)
1 | /** |
如何运行:
- 编译两个程序:
gcc writer.c -o writer和gcc reader.c -o reader。 - 在一个终端先运行
./reader,它会阻塞。 - 在另一个终端再运行
./writer,它会写入数据并退出 - 此时,
reader终端会打印出收到的消息并退出
(3)、通过信号 (Signal) 来通信
信号是一种异步通信机制,用于通知进程某个事件已经发生。它不传输数据,功能类似于一个软件中断,用于进程控制和状态通知。
1、信号命令与种类
A、信号的种类
- 查看命令:
man 7 signal或kill -l - 常用信号:
SIGINT (2): 中断信号 (Ctrl+C)。SIGKILL (9): 强制杀死信号,不可被捕捉或忽略。SIGTERM (15): 终止信号 (kill 默认发送)。SIGCHLD (17): 子进程状态改变。SIGSTOP (19): 暂停信号,不可被捕捉或忽略。SIGALRM (14): 定时器信号。SIGUSR1,SIGUSR2: 用户自定义信号。
B、终端发送信号
kill -<信号值> <PID>: 向指定进程发送信号。killall -<信号值> <进程名>: 向所有同名进程发送信号。
2、信号编程核心函数
kill(): 发送信号 (man 2 kill)
1 |
|
alarm(): 设置定时器信号 (man 2 alarm)
1 |
|
signal(): 设置信号处理方式 (man 2 signal)
1 |
|
3、示例代码
a、处理终止信号 (捕捉信号)
- 用途:允许程序在收到
Ctrl+C等终止信号时,执行清理工作(如保存数据)后再优雅地退出。 - 说明:捕捉
SIGINT和SIGQUIT信号。
1 |
|
b、处理闹钟信号 (捕捉信号)
- 用途:实现超时机制或周期性任务。
- 说明:使用
alarm()配合SIGALRM信号。
1 |
|
c、忽略信号
- 用途:保护关键进程不被某些信号(如
Ctrl+C)意外中断。
1 |
|
d、处理通信信号
- 用途:实现父子进程或任意两个进程间的简单通知。
1 |
|
e、处理子进程的退出
- 用途:父进程通过
SIGCHLD信号异步回收子进程资源,避免产生僵尸进程,且父进程无需阻塞。
1 |
|
f、处理自定义工作进程控制
- 用途:使用用户自定义信号
SIGUSR1和SIGUSR2来灵活控制一个工作进程的状态(如暂停/继续/终止)。
1 |
|
g、信号控制多线程 (暂无)
(此部分内容待后续线程章节学习后补充)
(4)、System V IPC(重点)
System V IPC 是一组经典且功能强大的进程间通信机制,包括消息队列、共享内存和信号量。它们共享一套相似的设计哲学和管理方式。
核心概念与管理
1. 工作流程
所有 System V IPC 机制都遵循相似的步骤:
- 创建密钥 (Key):使用
ftok()函数,通过一个已存在的文件路径和项目 ID,生成一个唯一的key_t类型的键值。这个键值是不同进程用来访问同一个 IPC 对象的“门牌号”。 - 创建/获取 IPC 对象:使用
msgget()、shmget()或semget()函数,传入密钥来创建新的 IPC 对象,或获取一个已存在的对象的 ID。 - 操作 IPC 对象:使用各自的 API(如
msgsnd/msgrcv,shmat,semop)进行数据交换或同步。 - 销毁 IPC 对象:通信结束后,使用
msgctl()、shmctl()或semctl()来删除 IPC 对象,释放内核资源。
2. IPC 管理命令
ipcs -a: 查看系统中所有的 IPC 对象(消息队列、共享内存、信号量)。ipcs -q: 单独查看消息队列。ipcs -m: 单独查看共享内存。ipcs -s: 单独查看信号量。ipcrm -q <id>: 删除指定 ID 的消息队列。ipcrm -m <id>: 删除指定 ID 的共享内存。ipcrm -s <id>: 删除指定 ID 的信号量。
3. ftok() 函数:创建密钥
man 3 ftok 查看接口函数。
1 |
|
A、消息队列 (Message Queue)
1. 说明与图解
消息队列是存放在内核中的一个消息链表,允许一个或多个进程向其发送或接收结构化的消息。它克服了管道只能传输无格式字节流的限制。
2. 核心函数
msgget(): 创建或获取消息队列 ID (man 2 msgget)1
2
3
int msgget(key_t key, int msgflg);
// msgflg: 表示创建或打开队列的权限和方式,通常是 IPC_CREAT | 0666msgsnd(): 发送消息 (man 2 msgsnd)1
2int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 参数分别为: 消息id 消息本体 消息大小 发送方式,一般为0msgrcv(): 接收消息 (man 2 msgrcv)1
2ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 参数分别为: 消息id 消息本体 消息大小 消息类型 发送方式,一般为0msgctl(): 控制消息队列 (man 2 msgctl)1
2int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// cmd: IPC_RMID 用于删除消息队列
3. 示例代码
公共结构体定义 (common.h)
1
2
3
4
5
6
7
8// common.h
struct msg_buf {
long mtype; // 消息类型,必须 > 0
char mtext[128]; // 消息内容
};发送端 (sender.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// sender.c
int main() {
key_t key = ftok(MSG_KEY_PATH, MSG_KEY_PROJ_ID);
int msqid = msgget(key, IPC_CREAT | 0666);
struct msg_buf msg = {1, "Hello from sender!"}; // 消息类型为 1
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
printf("消息已发送。\n");
return 0;
}接收端 (receiver.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// receiver.c
int main() {
key_t key = ftok(MSG_KEY_PATH, MSG_KEY_PROJ_ID);
int msqid = msgget(key, IPC_CREAT | 0666);
struct msg_buf msg;
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0); // 接收类型为 1 的消息
printf("收到消息: %s\n", msg.mtext);
// 删除消息队列
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
B、共享内存 (Shared Memory)
1. 说明与图解
共享内存是最快的 IPC 方式。它将一块物理内存映射到多个进程的虚拟地址空间中,进程可以直接像访问普通变量一样读写这块内存,无需内核介入数据拷贝。
2. 核心函数
shmget(): 创建或获取共享内存 ID (man 2 shmget)1
2
3
int shmget(key_t key, size_t size, int shmflg);
// size: 共享内存大小,通常是页大小的整数倍。shmat(): 映射共享内存到进程地址空间 (man 2 shmat)1
2void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值是映射到本进程的内存地址指针。shmdt(): 解除映射 (man 2 shmdt)1
int shmdt(const void *shmaddr);
shmctl(): 控制共享内存 (man 2 shmctl)1
2int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd: IPC_RMID 用于删除共享内存段。
3. 示例代码
写进程 (writer.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// writer.c
int main() {
key_t key = ftok(".", 'b');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
char *shm_ptr = (char *)shmat(shmid, NULL, 0);
strcpy(shm_ptr, "Hello from shared memory!");
printf("数据已写入共享内存。\n");
shmdt(shm_ptr); // 解除映射
return 0;
}读进程 (reader.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// reader.c
int main() {
key_t key = ftok(".", 'b');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
char *shm_ptr = (char *)shmat(shmid, NULL, 0);
printf("从共享内存读取数据: %s\n", shm_ptr);
shmdt(shm_ptr); // 解除映射
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
return 0;
}
C、信号量 (Semaphore)
1. 说明
信号量本质上是一个计数器,主要用于进程间的同步与互斥,以保护共享资源(如共享内存)不被并发访问破坏。它本身不传输数据。
- 互斥:确保同一时间只有一个进程能访问临界资源(如厕所只有一把钥匙)。
- 同步:协调多个进程的执行顺序(如生产者生产完,才能通知消费者消费)。
- PV 操作:
- P 操作 (
sem_wait): 申请资源,信号量计数值减 1。如果计数值为 0,则进程阻塞。 - V 操作 (
sem_post): 释放资源,信号量计数值加 1。
- P 操作 (
2. POSIX 有名信号量
这是比 System V 信号量更现代、更易用的接口,常用于进程间同步。它在 /dev/shm/ 目录下创建一个文件来表示信号量。
3. 核心函数 (编译时需链接 -pthread)
sem_open(): 创建或打开有名信号量 (man 3 sem_open)1
2
3
4
5
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
// name: 信号量名称,格式为 "/somename"。
// oflag: O_CREAT 创建, O_EXCL 确保是新创建。
// value: 信号量初始值。sem_wait(): P 操作 (加锁) (man 3 sem_wait)1
int sem_wait(sem_t *sem);
sem_post(): V 操作 (解锁) (man 3 sem_post)1
int sem_post(sem_t *sem);
sem_close(): 关闭信号量 (man 3 sem_close)1
int sem_close(sem_t *sem);
sem_unlink(): 删除有名信号量 (man 3 sem_unlink)1
int sem_unlink(const char *name);
4. 示例代码:使用信号量保护共享内存
公共头文件 (common.h)
1
2
3
4
5// common.h
初始化进程 (init.c)
1
2
3
4
5// init.c
// (包含 shmget/shmat/sem_open 等创建和初始化代码)
// sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 1); // 初始值为1,用于互斥
// ...使用共享资源的进程 (proc.c)
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// proc.c
int main() {
key_t key = ftok(SHM_KEY_PATH, SHM_KEY_PROJ_ID);
int shmid = shmget(key, sizeof(int), 0);
int *shared_counter = (int *)shmat(shmid, NULL, 0);
sem_t *sem = sem_open(SEM_NAME, 0);
for (int i = 0; i < 5; i++) {
sem_wait(sem); // P操作:加锁
// --- 临界区开始 ---
int temp = *shared_counter;
printf("PID %d: read %d, ", getpid(), temp);
temp++;
sleep(1); // 模拟耗时操作
*shared_counter = temp;
printf("write %d\n", *shared_counter);
// --- 临界区结束 ---
sem_post(sem); // V操作:解锁
}
shmdt(shared_counter);
sem_close(sem);
return 0;
}如何运行:先运行一个程序初始化共享内存(设为 0)和信号量(设为 1)。然后同时运行多个
proc实例。你会看到,由于信号量的保护,共享计数器会从 0、1、2…有序地增加,而不会出现多个进程同时读到相同值导致的“竞争条件”。





