进程间通信
一、管道(pipe)
1、概述
管道(pipe)又名匿名管道,是一种最基本的 IPC 机制,通过pipe
函数创建:
#include <unistd.h>
int pipe(int pipefd[2]); // 成功返回 0,失败返回 -1
调用pipe
函数时,操作系统在内核中开辟一块缓冲区(大小固定)用于通信,它有一个读端和一个写端,pipefd[0]
指向管道读端,pipefd[1]
指向管道写端,所以管道在用户程序看来就像打开一个文件,通过read(pipefd[0])
或者write(pipefd[1])
向这个文件读写数据,其实是在读写内核缓冲区。
2、流程
- 父进程调用
pipe
开辟管道,得到两个文件描述符分别指向管道的两端; - 父进程调用
fork
函数创建子进程,则子进程拷贝两个文件描述符指向同一管道; - 父进程关闭管道读端,子进程关闭管道写端,此时父进程可以往管道里写,子进程可以从管道里读,管道使用环形队列实现,数据从写端流入从读端流出,从而实现进程间通信。
3、特殊情况
-
写端关闭,读端打开
管道中剩余的数据都被读取后,再次
read
会返回 0,就像读到文件末尾一样。 -
写端打开,但是不写数据,读端打开
管道中剩余的数据都被读取之后再次
read
会被阻塞,直到管道中有数据可读了才重新读取数据并返回。 -
读端关闭,写端打开
此时进程收到信号
SIGPIPE
,通常会导致进程异常终止。 -
读端打开,但是不读取数据,写端打开
当写端被写满之后再次
write
会阻塞,直到管道中有空位置了才会写入数据并重新返回。
4、缺点
- 两个进程通过一个管道只能实现单向通信,如果想双向通信必须再创建一个管道或者使用
socketpair
; - 只能用于具有亲缘关系的进程间通信,如父子进程或兄弟进程。
5、示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int _pipe[2] = {0, 0};
// 创建管道
int ret = pipe(_pipe);
if (ret == -1)
{
perror("create pipe error");
return 1;
}
printf("_pipe[0] is %d,_pipe[1] is %d\n", _pipe[0], _pipe[1]);
// fork子进程
pid_t id = fork();
if (id < 0)
{
perror("fork error");
return 2;
}
else if (id == 0)
{
printf("child writing\n");
close(_pipe[0]);
int count = 5;
const char *msg = "i am child";
while (count--)
{
write(_pipe[1], msg, strlen(msg));
sleep(1);
}
close(_pipe[1]);
exit(1);
}
else
{
printf("father reading\n");
close(_pipe[1]);
char msg[1024];
int count = 5;
while (count--)
{
ssize_t s = read(_pipe[0], msg, sizeof(msg) - 1);
if (s > 0)
{
msg[s] = '\0';
printf("msg: %s\n", msg);
}
else
{
perror("read error");
exit(1);
}
}
if (waitpid(id, 0, NULL) != -1)
{
printf("wait success\n");
}
}
return 0;
}
二、命名管道(FIFO)
1、概述
命名管道(FIFO)的出现解决了管道(pipe)只能用于具有亲缘关系的进程之间通信的局限性,命名管道不同于管道之处在于它提供一个路径名与之关联,以FIFO
的文件形式存储在文件系统中,命名管道是一个设备文件因此即使进程与创建FIFO
的进程不存在亲缘关系,只要可以访问该路径,就能通过FIFO
相互通信。
2、创建
- 使用系统函数建立命名管道
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int mknod(const char *pathname, mode_t mode, dev_t dev);
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 参数
// pathname: 管道路径名
// mode: 管道模式,指明其存取权限
// dev: 设备值,取决于文件创建的种类,它旨在创建设备文件时才会用到
//
// 返回值
// 成功返回 0, 失败返回 -1
- 在 Shell 中通过
mknod
或mkfifo
命名创建管道
3、特点
- 命名管道时一个存储在硬盘上的文件,而管道是存储在内存中的特殊文件,所以使用命名管道之前必须先通过
open
函数打开文件; - 命名管道可以用于任何两个进程之间的通信,不管这两个进程之间是否具有亲缘关系。
4、示例
// Server
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 创建一个存取权限为 0666 的命名管道
int namepipe = mkfifo("myfifo", S_IFIFO | 0666);
if (namepipe == -1)
{
perror("mkfifo error");
exit(1);
}
// 打开命名管道
int fd = open("./myfifo", O_RDWR);
if (fd == -1)
{
perror("open error");
exit(2);
}
char buf[1024];
while (1)
{
printf("sendto# ");
fflush(stdout);
// 从标准输入获取消息
ssize_t s = read(0, buf, sizeof(buf) - 1);
if (s > 0)
{
// 过滤掉从标准输入中获取的换行
buf[s - 1] = '\0';
// 把该消息写入到命名管道中
if (write(fd, buf, s) == -1)
{
perror("write error");
exit(3);
}
}
}
close(fd);
return 0;
}
// Client
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./myfifo", O_RDWR);
if (fd == -1)
{
perror("open error");
exit(1);
}
char buf[1024];
while (1)
{
ssize_t s = read(fd, buf, sizeof(buf) - 1);
if (s > 0)
{
printf("client# %s\n", buf);
}
else // 读失败或者是读取到字符结尾
{
perror("read error");
exit(2);
}
}
close(fd);
return 0;
}
三、消息队列
1、概述
作用:从一个进程向另一个进程发送带有类型的数据块。
本质:在内核中实现的一个消息链表。
2、特点
-
每个数据库都被认为有一个类型,客户端进程接收数据块可以有不同的类型值;
-
和管道一样,每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总字节数也是有上限的(MSGMNB),系统上的消息队列总数也是有上限的(MSGMNI);
-
全双工通信
-
生命周期由内核控制
3、结构
-
查看消息队列命令:
ipcs -q
-
系统限制
#define MSGMNI 16
#define MSGMAX 8192
#define MSGMNB 16384
- IPC对象数据结构:
/usr/include/linux/ipc.h
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
- 消息队列结构:
/usr/include/linux/msg.h
struct msqid_ds
{
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
};
- 消息结构
struct msgbuf
{
long mtype; /* type of message */
char mtext[1]; /* message text */
};
4、接口
- 创建消息队列
#include <sys/msg.h>
// 参数
// key:内核中消息队列标识
// flag:一般传入 IPC_CREAT(不存在则创建,存在则打开)、IPC_EXCL(与 IPC_CREAT 同时使用,存在则报错)或读写权限
//
// 返回值
// 成功返回消息队列 ID,失败返回 -1
//
// 缺点
// 如果文件被删除或者替换,通过 key 值打开的不是同一个消息队列
int msgget(key_t key, int flag);
#include <sys/ipc.h>
// 作用
// 通过文件的 inode 节点号和一个 proj_id 计算出 key 值
//
// 返回值
// 成功返回 key,失败返回 (key_t)-1
key_t ftok(const char *path, int id);
- 发送 / 接收数据
#include <sys/msg.h>
// 发送,成功返回 0,失败返回 -1
int msgsend(int msgid, const void *ptr, size_t nbytes, int flag);
// 接收,成功返回接收长度,失败返回 -1
ssize_t msgrcv(int msgid, void *ptr, size_t nbytes, long type, int flag);
// 参数
// msgid:由 msgget 返回的消息队列 ID
// ptr:指向准备接收的消息结构体 msgbuf,需要自己定义
// nbytes:ptr 指向的数据长度,不包含 mtype
// type: 接收的数据类型,type == 0 返回队列中的第一个消息,type > 0 返回队列中消息类型为 type 的第一个消息,type < 0 返回队列中消息类型值小于等于 type 绝对值的信息(优先取最小值)
// flag:IPC_NOWAIT(使操作不阻塞,没有消息时返回 -1,error 设置为 ENOMSG),0(阻塞等待)
注:消息队列接收消息成功后内核更新与该消息队列相关联的msqid_ds
结构,刷新调用进程msg_lrpid
(进程 ID)、调用事件msg_rtime
并将消息数msg_qnum
减一。
- 控制消息队列行为
#include <sys/msg.h>
// 参数
// msgid:消息队列 ID
// cmd:IPC_STAT(取队列的 msqid_ds 放入 buf 指向的结构体中)、IPC_SET(将 msg_perm.uid、msg_perm.mode和msg_qbytes信息从 buf 结构体复制到这个队列的 msqid_ds 结构中)、IPC_RMID(从系统中删除该消息队列的所有数据)
//
// 返回值
// 成功返回 0,失败返回 -1
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
5、流程
6、示例
// Server
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/types.h>
#define IPC_KEY 0x12345678 // 消息队列 key
#define TYPE_SER 1 // 数据块类型
#define TYPE_CLI 2 // 数据块类型
struct msgbuf
{
long mtype;
char mtext[1024];
};
int main()
{
int msgid = -1;
msgid = msgget(IPC_KEY, IPC_CREAT | 0664);
if (msgid < 0)
{
perror("msgget error");
return -1;
}
while (1)
{
struct msgbuf buf;
// 发送数据
memset(&buf, 0x00, sizeof(struct msgbuf));
buf.mtype = TYPE_CLI;
scanf("%s", buf.mtext);
msgsnd(msgid, &buf, 1024, 0);
// 接收数据
msgrcv(msgid, &buf, 1024, TYPE_SER, 0);
printf("server say:[%s]\n", buf.mtext);
}
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
// Client
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/types.h>
#define IPC_KEY 0x12345678 // 消息队列 key
#define TYPE_SER 1 // 数据块类型
#define TYPE_CLI 2 // 数据块类型
struct msgbuf
{
long mtype;
char mtext[1024];
};
int main()
{
int msgid = -1;
msgid = msgget(IPC_KEY, IPC_CREAT | 0664);
if (msgid < 0)
{
perror("msgget error");
return -1;
}
while (1)
{
// 接收数据
struct msgbuf buf;
msgrcv(msgid, &buf, 1024, TYPE_CLI, 0);
printf("client say:[%s]\n", buf.mtext);
// 发送数据
memset(&buf, 0x00, sizeof(struct msgbuf));
buf.mtype = TYPE_SER;
scanf("%s", buf.mtext);
msgsnd(msgid, &buf, 1024, 0);
}
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
四、信号量
1、概述
什么是信号量
信号量本质是一种数据操作锁,用来负责数据操作过程中的互斥、同步等功能。信号量用来管理临界资源,它本身只是一种外部资源的标识,不具有数据交换功能,而是通过控制其他的通信资源实现进程间通信。 可以将信号量理解为一个计数器。当有进程对它所管理的资源进行请求时,进程先要读取信号量的值:大于 0,资源可以请求;等于 0,资源不可以用。这时进程会进入睡眠状态直至资源可用。
当一个进程不再使用资源时,信号量 +1(对应的操作称为 V 操作),反之当有进程使用资源时,信号量 -1(对应的操作为 P 操作),对信号量进行操作时均为原子操作。
为什么使用信号量
为了防止出现因多个进程同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域(临界资源:一次只允许一个进程使用的资源;临界区:访问临界资源的程序代码片段)。
2、原理
P(sv)
:如果 sv 的值大于零,就给它 -1;如果它的值为 0,就挂起该进程的执行等待操作。
V(sv)
:如果有其他进程因等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而挂起,就给它 +1。
举个例子,就是两个进程共享信号量 sv(假设初始值为 1),一旦其中一个进程执行了P(sv)
操作,它将得到信号量,并可以进入临界区,使 sv--,而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)
时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)
释放信号量,这时第二个进程就可以恢复执行了。
3、接口
- 创建 / 获取一个信号量集合
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 参数
// key:使用 ftok() 获取
// nsems:需要创建的信号量集合中信号量个数(信号量只能以集合形式创建)
// semflg:同时使用 IPC_CREAT 和 IPC_EXCL 则会创建一个新的信号量集合,若已经存在的话返回 -1;单独使用 IPC_CREAT 会返回一个新的或者已经存在的信号量集合
//
// 返回值
// 成功返回信号量集合的 semid,失败返回 -1
int semget(key_t key, int nsems, int semflg);
- 改变信号量的值
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
// 参数
// semid:信号量集合的 id
// sops:指向存储信号操作结构的数组指针,信号操作结构的原型如上
// nsops:信号操作结构的数量,恒大于或等于 1
// timeout:当 semtimedop() 调用致使进程进入睡眠时,睡眠时间不能超过本参数指定的值。如果睡眠超时,semtimedop() 将失败返回,并设定错误值为 EAGAIN。如果本参数的值为 NULL,semtimedop() 将永远睡眠等待
//
// 返回值
// 成功返回 0,失败返回 -1
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops, struct timespec *timeout);
- 执行在信号量集合上的控制操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 参考 <bits/sem.h> 说明的 semun 结构
union semun
{
int val; // 使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL、SETALL 使用的缓存区
struct seminfo *__buf; // IPC_INFO(linux 特有)使用缓存区
};
// 作用
// 执行在信号量集合上的控制操作
//
// 参数
// semnum:信号量集合 semid 中的被操作信号量编号(从 0 开始)
// cmd:IPC_STAT(读取一个信号量集合的数据结构 semid_ds,并将其存储在 semun 的 buf 中)、IPC_SET(设置信号量集的数据结构 semid_ds 中的元素 ipc_perm,其值取自 semun 的 buf 参数)和 IPC_RMID(将信号量集从内存中删除)
// arg:一个 semun 实例(是一个联合类型的副本,而不是一个指向联合类型的指针,最好自己定义否则编译器可能报错 semun 大小未知)
//
// 返回值
// 成功返回 0,失败返回 -1
int semctl(int semid, int semnum, int cmd, ...);
4、示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/wait.h>
#define PATHNAME "."
#define PROJID 0x6666
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */
};
static CommSemSet(int num, int flag)
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
perror("ftok error");
exit(1);
}
int sem_id = semget(key, num, flag);
if (sem_id == -1)
{
perror("semget error");
exit(2);
}
return sem_id;
}
// 创建信号量集合
int CreateSemSet(int num)
{
return CommSemSet(num, IPC_CREAT | IPC_EXCL | 0666);
}
// 初始化信号量
int InitSem(int sem_id, int which)
{
union semun un;
un.val = 1;
int ret = semctl(sem_id, which, SETVAL, un);
if (ret < 0)
{
perror("semctl");
return -1;
}
return 0;
}
// 获取信号量集合
int GetSemSet()
{
return CommSemSet(0, IPC_CREAT);
}
static int SemOp(int sem_id, int which, int op)
{
struct sembuf buf;
buf.sem_num = which;
buf.sem_op = op;
buf.sem_flg = 0;
int ret = semop(sem_id, &buf, 1);
if (ret < 0)
{
perror("semop error");
return -1;
}
return 0;
}
// P 操作
int P(int sem_id, int which)
{
return SemOp(sem_id, which, -1);
}
// V 操作
int V(int sem_id, int which)
{
return SemOp(sem_id, which, 1);
}
// 销毁信号量集合
int DestroySemSet(int sem_id)
{
int ret = semctl(sem_id, 0, IPC_RMID);
if (ret < 0)
{
perror("semctl error");
return -1;
}
return 0;
}
int main()
{
//创建信号量
int sem_id = CreateSemSet(1);
InitSem(sem_id, 0);
pid_t id = fork();
if (id < 0)
{
perror("fork error");
exit(1);
}
else if (id == 0) // 子进程打印 AA
{
printf("child is running, pid = %d, ppid = %d\n", getpid(), getppid());
while (1)
{
P(sem_id, 0);
printf("A");
usleep(10031);
fflush(stdout);
printf("A");
usleep(10021);
fflush(stdout);
V(sem_id, 0);
}
}
else // 父进程打印 BB
{
printf("father is running, pid = %d, ppid = %d\n", getpid(), getppid());
while (1)
{
P(sem_id, 0);
printf("B");
usleep(10051);
fflush(stdout);
printf("B");
usleep(10003);
fflush(stdout);
V(sem_id, 0);
}
wait(NULL);
}
DestroySemSet(sem_id);
return 0;
}
五、共享内存
1、原理
2、接口
- 创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
// 参数
// size:申请共享内存的大小,一般是 4k 的整数倍
// shmflg:IPC_CREAT 和 IPC_EXCL 一起使用创建一个新的共享内存,否则返回 -1;单独使用 IPC_CREAT 返回一个共享内存,有则直接返回否则新建
//
// 返回值
// 成功返回共享内存 id,失败返回 -1
int shmget(key_t key, size_t size, int shmflg);
- 挂接
#include <sys/shm.h>
void* shmat(int shmid); // 返回挂接内存的虚拟首地址,其作用是将申请的共享内存挂接到进程页表,将虚存和物理内存相对应
- 去挂接
#include <sys/shm.h>
int shmdt(const void *shmaddr); // 传入被挂接内存的虚拟首地址,其作用是将共享内存从进程页表剥离,去除映射关系
- 对共享内存的控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd:
// IPC_STAT(得到共享内存的状态,把共享内存的 shmid_ds 结构复制到 buf 中)
// IPC_SET(改变共享内存的状态,把 buf 所指 shmid_ds 结构中的 uid、gid、mode 复制到共享内存的 shmid_ds.ipc_perm 结构内)
// IPC_RMID(删除这片共享内存)
//
// buf:共享内存管理结构体(<bits/shm.h>内定义如下)
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation permission struct */
size_t shm_segsz; /* size of segment in bytes */
__time_t shm_atime; /* time of last shmat() */
#ifndef __x86_64__
unsigned long int __unused1;
#endif
__time_t shm_dtime; /* time of last shmdt() */
#ifndef __x86_64__
unsigned long int __unused2;
#endif
__time_t shm_ctime; /* time of last change by shmctl() */
#ifndef __x86_64__
unsigned long int __unused3;
#endif
__pid_t shm_cpid; /* pid of creator */
__pid_t shm_lpid; /* pid of last shmop */
shmatt_t shm_nattch; /* number of current attaches */
__syscall_ulong_t __unused4;
__syscall_ulong_t __unused5;
};
3、示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "."
#define PROCID 0x6666
#define SIZE 4096 * 1
static int CommShm(int flag)
{
key_t key = ftok(PATHNAME, PROCID);
if (key < 0)
{
perror("ftok");
return -1;
}
int shm_id = shmget(key, SIZE, flag);
if (shm_id < 0)
{
perror("shmget");
return -2;
}
return shm_id;
}
int CreatShm()
{
return CommShm(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return CommShm(IPC_CREAT);
}
int DestroyShm(int shm_id)
{
int ret = shmctl(shm_id, IPC_RMID, NULL);
if (ret < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
// Server
int main()
{
int shm_id = CreatShm();
printf("shm_id = %d\n", shm_id);
char *mem = (char *)shmat(shm_id, NULL, 0);
while (1)
{
sleep(1);
printf("%s\n", mem);
}
shmdt(mem);
DestroyShm(shm_id);
return 0;
}
// Client
int main()
{
int shm_id = GetShm();
char *mem = (char *)shmat(shm_id, NULL, 0);
int index = 0;
while (1)
{
sleep(1);
mem[index++] = 'A';
index %= (SIZE - 1);
mem[index] = '\0';
}
shmdt(mem);
DestroyShm(shm_id);
return 0;
}
4、优点
共享内存是这五种进程间通信方式中效率最高的。但是因为共享内存没有提供相应的互斥机制,所以一般共享内存都和信号量配合起来使用。
高效的原因
消息队列,FIFO及管道的消息传递方式一般为 :
- 服务器获取输入的信息;
- 通过管道,消息队列等写入数据至内存中,通常需要将该数据拷贝到内核中;
- 客户从内核中将数据拷贝到自己的客户端进程中;
- 然后再从进程中拷贝到输出文件;
上述过程通常要经过4次拷贝,才能完成文件的传递,而共享内存只需要:
- 输入内容到共享内存区
- 从共享内存输出到文件
上述过程不涉及到内核的拷贝,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间,提高了效率。