示例代码
- github: https://github.com/lengjingzju/notes/tree/master/code/linux_ipc
- gitee: https://gitee.com/lengjingzju/notes/tree/master/code/linux_ipc
一、System V IPC概述
System V IPC(Inter-Process Communication)是Unix系统V版本中引入的一组进程间通信机制,包括:
- 消息队列(Message Queue):进程间传递结构化数据
- 信号量(Semaphore):进程同步机制
- 共享内存(Shared Memory):进程间高效共享数据
System V IPC具有以下特点:
- 持久性:IPC对象在内核中持久存在,直到显式删除或系统重启
- 全局性:所有进程通过相同的key访问同一IPC对象
- 权限控制:通过IPC权限结构控制访问权限
- 丰富的控制操作:提供多种控制命令管理IPC对象
这些机制在早期的Unix系统中被广泛使用,虽然POSIX IPC提供了更现代的实现,但System V IPC因其悠久历史和广泛支持,仍在许多系统中使用。
System V IPC接口表如下:
| 接口 | 消息队列 | 信号量 | 共享内存 |
|---|---|---|---|
| 头文件 | <sys/msg.h> | <sys/sem.h> | <sys/shm.h> |
| 描述符 | msqid_ds | semid_ds | shmid_ds |
| 创建/打开 | msgget() | semget() | shmget() + shmat() |
| 关闭对象 | 无 | 无 | shmdt() |
| 控制操作 | msgctl() | semctl() | shmctl() |
| 执行IPC | msgsnd() / msgrcv() | msgop() | |
| 发送/接收 消息 | 测试/调整 信号量 | 操作共享内存 |
二、System V IPC通用概念
1. IPC键值(key_t)
每个System V IPC对象都通过一个唯一的键值标识。可以通过以下方式生成键值:
c
/**
* @brief 生成System V IPC键值
* @param pathname [IN] 现有文件的路径名
* @param proj [IN] 项目标识符(低8位有效)
* @return 成功返回键值,失败返回-1
* @note 基于文件的inode号、设备号和项目标识符生成键值。
* 不同文件可能生成相同键值,删除重建文件会改变inode,从而改变键值。
*/
key_t ftok(const char *pathname, int proj);
2. IPC权限结构
所有System V IPC对象都包含一个ipc_perm结构,用于权限控制:
c
struct ipc_perm {
key_t __key; /* 提供给get调用的键值 */
uid_t uid; /* 所有者的用户ID */
gid_t gid; /* 所有者的组ID */
uid_t cuid; /* 创建者的用户ID */
gid_t cgid; /* 创建者的组ID */
unsigned short mode; /* 权限位 */
unsigned short __seq; /* 序列号 */
};
3. IPC创建标志
创建IPC对象时使用的标志:
IPC_CREAT:如果对象不存在则创建IPC_EXCL:与IPC_CREAT一起使用,确保创建的是新对象
三、System V消息队列
消息队列允许进程以消息的形式交换数据,消息具有类型和内容,可以按类型接收消息。
1. 接口说明
c
/**
* @brief 创建或打开消息队列
* @param key [IN] IPC键值,可用ftok生成或IPC_PRIVATE
* @param msgflg [IN] 创建标志和权限,如IPC_CREAT|IPC_EXCL|0666
* @return 成功返回消息队列标识符,失败返回-1
* @note 使用IPC_PRIVATE会创建一个新的消息队列,只有知道标识符的进程可以访问。
*/
int msgget(key_t key, int msgflg);
/**
* @brief 向消息队列发送消息
* @param msqid [IN] 消息队列标识符
* @param msgp [IN] 指向消息结构的指针
* @param msgsz [IN] 消息正文的长度(不含消息类型长度)
* @param msgflg [IN] 发送标志,如IPC_NOWAIT(非阻塞)
* @return 成功返回0,失败返回-1
* @note 消息结构必须包含long类型的mtype字段和字符数组mtext。
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
/**
* @brief 从消息队列接收消息
* @param msqid [IN] 消息队列标识符
* @param msgp [OUT] 指向消息结构的指针
* @param maxmsgsz [IN] 消息缓冲区的最大长度
* @param msgtyp [IN] 要接收的消息类型
* @param msgflg [IN] 接收标志,如IPC_NOWAIT(非阻塞)
* @return 成功返回接收到的消息长度,失败返回-1
* @note msgtyp为0表示接收第一条消息,>0表示接收指定类型的消息,
* <0表示接收类型小于等于绝对值的消息中类型最小的消息。
*/
ssize_t msgrcv(int msqid, void *msgp, size_t maxmsgsz, long msgtyp, int msgflg);
/**
* @brief 控制消息队列
* @param msqid [IN] 消息队列标识符
* @param cmd [IN] 控制命令
* @param buf [INOUT] 指向msqid_ds结构的指针
* @return 成功返回0,失败返回-1
* @note 常用命令:IPC_RMID(删除队列),IPC_STAT(获取状态),IPC_SET(设置参数)
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
消息数据结构:
c
struct msgbuf {
long mtype; /* 消息类型,必须大于0 */
char mtext[1]; /* 消息正文,长度可变 */
};
2. 代码示例
以下是一个使用System V消息队列的例子,父进程从终端读取数据,发送到消息队列,子进程从消息队列接收数据,输出到终端。输入"q"退出。
代码链接:test_sysv_msg.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "jlog_core.h"
#define BUF_SIZE 128
#define TIP(str) write(STDOUT_FILENO, str, strlen(str))
#define SYSV_MSG_TYPE 1
#define SYSV_MSG_PATH "/tmp/test_sysv_msg"
#define SYSV_MSG_PROJ 123
struct mbuf {
long mtype;
char mtext[BUF_SIZE];
};
static int read_from_sysv_msg(void)
{
struct mbuf msg;
key_t key;
int msgid;
ssize_t num;
int ret = 0;
usleep(1000);
key = ftok(SYSV_MSG_PATH, SYSV_MSG_PROJ);
msgid = msgget(key, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (msgid == -1) {
LLOG_ERRNO("msgget(%s) failed!\n", SYSV_MSG_PATH);
return -1;
}
for (;;) {
memset(msg.mtext, 0, BUF_SIZE);
num = msgrcv(msgid, &msg, BUF_SIZE, SYSV_MSG_TYPE, 0);
if (num == -1) {
LLOG_ERRNO("msgrcv() failed!\n");
ret = -1;
break;
}
if (num == 0)
continue;
if (num == 2 && msg.mtext[0] == 'q')
break;
TIP("msgrcv: ");
if (write(STDOUT_FILENO, msg.mtext, num) != num) {
LLOG_ERRNO("write STDOUT failed!\n");
ret = -1;
break;
}
}
return ret;
}
static int write_to_sysv_msg(void)
{
struct mbuf msg;
key_t key;
int msgid;
ssize_t num;
int ret = 0;
key = ftok(SYSV_MSG_PATH, SYSV_MSG_PROJ);
msgid = msgget(key, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (msgid == -1) {
LLOG_ERRNO("msgget(%s) failed!\n", SYSV_MSG_PATH);
return -1;
}
msg.mtype = SYSV_MSG_TYPE;
for (;;) {
usleep(1000);
TIP("msgsnd: ");
memset(msg.mtext, 0, BUF_SIZE);
num = read(STDIN_FILENO, msg.mtext, BUF_SIZE);
if (num == -1) {
LLOG_ERRNO("read STDIN failed!\n");
ret = -1;
break;
}
if (num == 0)
continue;
if (msgsnd(msgid, &msg, num, 0) == -1) {
LLOG_ERRNO("msgsnd() failed!\n");
ret = -1;
break;
}
if (num == 2 && msg.mtext[0] == 'q')
break;
}
msgctl(msgid, IPC_RMID, 0);
return ret;
}
int main(int argc, char *argv[])
{
int ret = 0;
SLOG_INFO("SYSV MSG: 父进程从终端写入获取数据发送消息,子进程接收消息将数据输出到终端。\n");
SLOG_INFO("[input q to exit]\n");
switch (fork()) {
case -1:
LLOG_ERRNO("fork() failed!\n");
exit(EXIT_FAILURE);
case 0: /* Child */
ret = read_from_sysv_msg();
SLOG_INFO("child process exit!\n");
exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
default: /* Parent */
ret = write_to_sysv_msg();
wait(NULL); /* wait for child process exit */
SLOG_INFO("parent process exit!\n");
exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}
}
3. 详细讲解
工作原理:
- 使用
ftok基于文件路径和项目标识符生成唯一的key。 - 父进程调用
msgget创建消息队列(使用IPC_CREAT|IPC_EXCL确保创建新队列)。 - 父进程从标准输入读取数据,使用
msgsnd发送消息到队列,消息类型为SYSV_MSG_TYPE。 - 子进程调用
msgget打开消息队列,使用msgrcv接收指定类型的消息。 - 通信完成后,父进程使用
msgctl删除消息队列。
注意事项:
- 消息队列中的消息是有类型的,接收时可以按类型接收。
- 消息队列在内核中持久存在,需要显式删除,否则会一直占用系统资源。
- 默认情况下,
msgsnd和msgrcv是阻塞的,可以设置IPC_NOWAIT标志使其非阻塞。 - 消息队列的最大消息数和每条消息的最大长度有限制,可以通过
msgctl的IPC_SET命令修改。
四、System V信号量
信号量是一种同步机制,用于协调多个进程对共享资源的访问。System V信号量以信号量集的形式存在,可以同时操作多个信号量。
1. 接口说明
c
/**
* @brief 创建或打开信号量集
* @param key [IN] IPC键值
* @param nsems [IN] 信号量集中信号量的数量
* @param semflg [IN] 创建标志和权限
* @return 成功返回信号量集标识符,失败返回-1
* @note nsems必须大于0。Linux中信号量默认初始值为0,因此需要显式初始化。
*/
int semget(key_t key, int nsems, int semflg);
/**
* @brief 信号量操作
* @param semid [IN] 信号量集标识符
* @param sops [IN] 操作结构数组
* @param nsops [IN] 操作数组的大小
* @return 成功返回0,失败返回-1
* @note 每个sembuf结构指定对哪个信号量进行何种操作。
*/
int semop(int semid, struct sembuf *sops, unsigned int nsops);
/**
* @brief 控制信号量
* @param semid [IN] 信号量集标识符
* @param semnum [IN] 信号量编号(有些命令忽略此参数)
* @param cmd [IN] 控制命令
* @param arg [IN] 命令参数
* @return 成功返回值依赖于cmd,失败返回-1
* @note 常用命令:IPC_RMID(删除集合),GETVAL/SETVAL(获取/设置单个信号量值),
* GETALL/SETALL(获取/设置所有信号量值)。
*/
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
操作结构:
c
struct sembuf {
unsigned short sem_num; /* 信号量编号 */
short sem_op; /* 操作值 */
short sem_flg; /* 操作标志 */
};
其中sem_op:
- 正数:信号量值增加该数值
- 0:等待信号量值变为0
- 负数:信号量值减少该数值的绝对值,如果信号量值不足会阻塞
2. 详细讲解
工作原理:
- 使用
semget创建或打开信号量集。 - 使用
semctl的SETVAL或SETALL命令初始化信号量值。 - 进程使用
semop对信号量进行操作:P操作(获取资源):sem_op为负数,减少信号量值V操作(释放资源):sem_op为正数,增加信号量值- 等待信号量变为0:
sem_op为0
- 使用
semctl的IPC_RMID命令删除信号量集。
注意事项:
- System V信号量以集合形式存在,可以同时操作多个信号量,实现复杂同步逻辑。
semop操作是原子的,要同时修改的信号量要么都成功,要么都不成功。- 可以使用
SEM_UNDO标志,使进程意外终止时自动撤销信号量操作。 - 信号量值永远不会小于0,尝试减小到小于0的操作会阻塞,直到有进程增加信号量值。
五、System V共享内存
共享内存允许多个进程共享同一块物理内存,是最快的IPC机制。但需要额外的同步机制(如信号量)来保护共享数据。
1. 接口说明
c
/**
* @brief 创建或打开共享内存段
* @param key [IN] IPC键值
* @param size [IN] 共享内存段大小(字节)
* @param shmflg [IN] 创建标志和权限
* @return 成功返回共享内存段标识符,失败返回-1
* @note 大小会被上舍入到系统分页大小的整数倍。
*/
int shmget(key_t key, size_t size, int shmflg);
/**
* @brief 将共享内存段映射到进程地址空间
* @param shmid [IN] 共享内存段标识符
* @param shmaddr [IN] 指定映射地址(通常为NULL,由系统选择)
* @param shmflg [IN] 映射标志,如SHM_RDONLY(只读映射)
* @return 成功返回映射地址,失败返回(void*)-1
* @note 调用成功后,进程可以通过返回的指针访问共享内存。
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
/**
* @brief 解除共享内存映射
* @param shmaddr [IN] 共享内存映射地址
* @return 成功返回0,失败返回-1
* @note 进程退出时会自动解除映射。
*/
int shmdt(const void *shmaddr);
/**
* @brief 控制共享内存段
* @param shmid [IN] 共享内存段标识符
* @param cmd [IN] 控制命令
* @param buf [INOUT] 指向shmid_ds结构的指针
* @return 成功返回0,失败返回-1
* @note 常用命令:IPC_RMID(标记删除),IPC_STAT(获取状态),IPC_SET(设置参数)
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
2. 代码示例
以下是一个使用System V共享内存和信号量的例子,父进程从终端读取数据写入共享内存,子进程从共享内存读取数据输出到终端。使用两个信号量同步读写。
代码链接:test_sysv_shm.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "jlog_core.h"
#define BUF_SIZE 128
#define TIP(str) write(STDOUT_FILENO, str, strlen(str))
#define SYSV_SHM_SIZE (8192 * 1)
#define SYSV_SEM_NUM 2
#define SYSV_SEMCTRL(idx, op) do { \
sops.sem_num = idx; \
sops.sem_op = op; \
sops.sem_flg = 0; \
semop(semid, &sops, 1); \
} while (0)
#define SYSV_SEMWAIT(sel) SYSV_SEMCTRL(sel, -1)
#define SYSV_SEMPOST(sel) SYSV_SEMCTRL(sel, 1)
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
#if defined(__linux__)
struct seminfo *__buf;
#endif
};
static int read_from_sysv_shm(int shmid, int semid)
{
struct sembuf sops;
char buf[BUF_SIZE];
char *addr;
ssize_t num;
int bflag = 0;
int ret = 0;
addr = shmat(shmid, NULL, 0);
if ((void*)addr == (void*)-1) {
LLOG_ERRNO("shmat() failed!\n");
return -1;
}
for (;;) {
SYSV_SEMWAIT(0);
memset(buf, 0, BUF_SIZE);
memcpy(buf, addr, BUF_SIZE);
num = strlen(buf);
if (num == 0) {
goto post;
}
if (num == 2 && buf[0] == 'q') {
bflag = 1;
goto post;
}
TIP("read SHM: ");
if (write(STDOUT_FILENO, buf, num) != num) {
LLOG_ERRNO("write STDOUT failed!\n");
bflag = 1;
ret = -1;
}
post:
SYSV_SEMPOST(1);
if (bflag)
break;
}
shmdt(addr);
return ret;
}
static int write_to_sysv_shm(int shmid, int semid)
{
struct sembuf sops;
char buf[BUF_SIZE];
char *addr;
ssize_t num;
int bflag = 0;
int ret = 0;
addr = shmat(shmid, NULL, 0);
if ((void*)addr == (void*)-1) {
LLOG_ERRNO("shmat() failed!\n");
return -1;
}
for (;;) {
SYSV_SEMWAIT(1);
TIP("write SHM: ");
memset(buf, 0, BUF_SIZE);
num = read(STDIN_FILENO, buf, BUF_SIZE);
if (num == -1) {
LLOG_ERRNO("read STDIN failed!\n");
bflag = 1;
ret = -1;
goto post;
}
if (num == 0) {
goto post;
}
memcpy(addr, buf, BUF_SIZE);
if (num == 2 && buf[0] == 'q') {
bflag = 1;
}
post:
SYSV_SEMPOST(0);
if (bflag)
break;
}
shmdt(addr);
return ret;
}
int main(int argc, char *argv[])
{
union semun args[2];
int semid;
int shmid;
int ret = 0;
SLOG_INFO("SYSV SHM: 父进程从终端写入获取数据到共享内存,子进程读取共享内存将数据输出到终端。\n");
SLOG_INFO("[input q to exit]\n");
shmid = shmget(IPC_PRIVATE, SYSV_SHM_SIZE, IPC_CREAT | IPC_EXCL
| S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (shmid == -1) {
LLOG_ERRNO("shmget() failed!\n");
exit(EXIT_FAILURE);
}
semid = semget(IPC_PRIVATE, SYSV_SEM_NUM, IPC_CREAT | IPC_EXCL
| S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (semid == -1) {
shmctl(shmid, IPC_RMID, 0);
LLOG_ERRNO("semget() failed!\n");
exit(EXIT_FAILURE);
}
args[0].val = 0;
args[1].val = 1;
if (semctl(semid, 0, SETVAL, args[0]) == -1
|| semctl(semid, 1, SETVAL, args[1]) == -1) {
semctl(semid, 0, IPC_RMID);
semctl(semid, 1, IPC_RMID);
shmctl(shmid, IPC_RMID, NULL);
LLOG_ERRNO("semctl() failed!\n");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1:
semctl(semid, 0, IPC_RMID);
semctl(semid, 1, IPC_RMID);
shmctl(shmid, IPC_RMID, NULL);
LLOG_ERRNO("fork() failed!\n");
exit(EXIT_FAILURE);
case 0: /* Child */
ret = read_from_sysv_shm(shmid, semid);
SLOG_INFO("child process exit!\n");
exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
default: /* Parent */
ret = write_to_sysv_shm(shmid, semid);
wait(NULL); /* wait for child process exit */
semctl(semid, 0, IPC_RMID);
semctl(semid, 1, IPC_RMID);
shmctl(shmid, IPC_RMID, NULL);
SLOG_INFO("parent process exit!\n");
exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}
}
3. 详细讲解
工作原理:
- 使用
shmget创建共享内存段,使用IPC_PRIVATE确保创建新段。 - 使用
semget创建包含两个信号量的信号量集,用于读写同步。 - 父进程和子进程分别使用
shmat将共享内存映射到自己的地址空间。 - 使用两个信号量实现生产者-消费者同步:
- 信号量0:读者信号量,初始为0,表示没有数据可读
- 信号量1:写者信号量,初始为1,表示可以写入数据
- 父进程写入数据前等待写者信号量,写入后释放读者信号量。
- 子进程读取数据前等待读者信号量,读取后释放写者信号量。
- 通信完成后,父进程删除共享内存段和信号量集。
注意事项:
- 共享内存是最快的IPC机制,因为数据直接在进程间共享,无需内核复制。
- 但共享内存没有内置同步机制,需要额外的同步机制(如信号量)保护共享数据。
- 共享内存映射后,进程可以通过指针直接访问内存,就像访问普通内存一样。
- 需要小心处理共享内存中的数据格式和字节序问题,特别是跨不同架构的系统。
- 共享内存段在内核中持久存在,需要显式删除,否则会一直占用内存资源。
六、总结
System V IPC提供了一组强大但稍显古老的进程间通信机制:
- 消息队列:适合传递结构化消息,支持按类型接收消息,但性能不如共享内存。
- 信号量:强大的同步机制,支持信号量集和原子操作多个信号量,但API较复杂。
- 共享内存:最快的IPC机制,适合大量数据共享,但需要额外的同步机制。
优点:
- 功能强大,历史悠久,广泛支持
- IPC对象在内核中持久存在
- 提供丰富的控制操作
缺点:
- API较复杂,学习曲线陡峭
- 需要显式删除IPC对象,否则会一直占用系统资源
- 某些设计(如ftok)在现代系统中可能不够可靠
使用建议:
- 对于新项目,优先考虑POSIX IPC,其接口更简单、更符合现代编程习惯
- 对于需要与旧系统兼容的项目,System V IPC仍然是必须的
- 根据具体需求选择合适的机制:少量数据用消息队列,大量数据用共享内存,同步用信号量
在下一篇文章中,我们将介绍POSIX IPC,它是更现代、更符合标准化的IPC机制,接口更简洁,功能更强大。