一文搞懂进程间常用通信方式(管道通信、消息队列、共享内存、信号量)
进程间的通信应用是很广泛的,比如后台进程和 GUI 界面数据传递,发送信号关机,Ctrl+C 终止正在运行的程序等。
Linux 进程间通信机制分三类:数据交互,同步,信号。理解了这些机制才能灵活运用操作系统提供的 IPC 工具。
本章以常用的管道(包括有名管道和无名管道),System V IPC(消息队列,共享内存,信号灯)为例来说明 Linux 进程通信常用的方法。
1.Linux内核提供的进程间通信方式
- 管道通信:无名管道和有名管道
- IPC通信:消息队列、共享内存、信号量
- 信号通信:比较复杂的通信方式,一般用于通知接收进程某个事件已经发生。
- socket域套接字
- 文件锁:与信号量类似,用于进程间对同一文件的互斥访问。
2.管道通信
2.1无名管道
(1)原理:内核维护的一块内存,有读端和写端。
(2)方法:父进程创建管道后fork子进程,子进程继承父进程的管道fd。
(3)限制:
1.只能在有关联的进程间通信,在目录中看不到管道文件节点,读写文件描述符存在于一个int型数组中。
2.半双工,只能单向传输数据,一个进程读,另一个进程只能写,读出来的字节顺序与写入的顺序一样。
(4)函数:pipe、write、read、close。
函数原型:int pipe(int pipefd[2]);
功能:创建无名管道
头文件:#include <unistd.h>
参数 pipefd[2]:一个 int 型数组,表示管道的文件描述符,pipefd[0]为读,pipefd[1]为写
返回值:成功返回 0,失败返回-1
(5)实验代码:创建无名管道,实现父进程与子进程间的通信;父进程将数据写入管道,子进程从管道读取数据并打印出来。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
char buf[32] = {0};
pid_t pid;
int fd[2];//定义一个变量来保存文件描述符,因为一个读端,一个写端,所以数量为 2 个
pipe(fd);//创建无名管道
print("fd[0] is %d\n", fd[0]);
print("fd[1] is %d\n", fd[1]);
pid = fork();//创建子进程
if (pid < 0)
{
printf("fork error\n");
}
if (pid > 0)
{
int status;
close(fd[0]);//在父进程中关闭读端
write(fd[1], "hello", 5);//在父进程中通过写端向管道写数据
close(fd[1]);//写完数据关闭写端
wait(&status);//等待子进程退出
exit(0);
}
if (pid == 0)
{
close(fd[1]);//关闭写端
read(fd[0], buf, 32);//从读端读信息
printf("buf is %s\n", buf);
close(fd[0]);//关闭读端
exit(0);
}
return 0;
}
运行结果:
、
2.2 有名管道
(1)原理:实质也是内核维护的一块内存,表现为一个有名字的文件。
(2)方法:固定一个文件名,两个进程分别使用mkfifo创建fifo文件,然后分别open打开获取fd,然后一个读一个写。
(3)特点:1.可以使无关联的进程通过fifo文件描述符进行数据传递。2.半双工方式,有一个写入端,一个读出端。
(4)函数:mkfifo、open、write、read、close。
函数原型:int mkfifo(const char *pathname, mode_t mode);
头文件:#include <sys/types.h>
#include <sys/stat.h>
参数 pathname: 有名管道的路径
参数 mode: 权限
返回值 成功返回 0,失败返回-1
(5)实验代码:使用mkfifo创建文件描述符,打开管道文件描述符,通过读写文件描述符进行单向数据传输。
/ifo_write.c/
/*ifo_write.c*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int ret;
char buf[32] = {0};
int fd;
if (argc < 2)
{
printf("Usage:%s <fifo name>\n", argv[0]);
return -1;
}
if (access(argv[1], F_OK) == 1)
{
ret = mkfifo(argv[1], 0666);
if (ret == -1)
{
printf("mkfifo error\n");
return -2;
}
printf("mkfifo success\n");
}
fd = open(argv[1], O_WRONLY);
while(1)
{
sleep(1);
write(fd, "hello", 5);
}
close(fd);
return 0;
}
/fifo_read.c/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[32] = {0};
int fd;
if (argc < 2)
{
printf("Usage:%s <fifo name> \n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDONLY);
while (1)
{
sleep(1);
read(fd, buf, 32);
printf("buf is %s\n", buf);
memset(buf, 0, sizeof(buf));//memset:在一段内存块中填充某个给定的值
}
close(fd);
return 0;
}
运行结果:
3.IPC通信
这些 IPC 对象存在于内核空间,应用层使用 IPC 通信的步骤为:
- 获取 key 值,内核会将 key 值映射成 IPC 标识符,获取 key 值常用方法: (1)在 get 调用中将 IPC_PRIVATE 常量作为 key 值。 (2)使用 ftok()生成 key
- 执行 IPC get 调用,通过 key 获取整数 IPC 标识符 id,每个 id 表示一个 IPC 对象
- 通过 id 访问 IPC 对象
- 通过 id 控制 IPC 对象
创建这三种 IPC 对象都要先获取 key 值,然后根据 key 获取 id,用到的函数如下:
函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1
复制代码
3.1共享内存
(1)共享内存和其他进程间数据交互方式相比,有以下几个突出特点:
- 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程, 所以效率和前面几个相比是最快的,可以用来进行批量数据的传输。
- 没有同步机制,需要借助 Linux 提供其他工具来进行同步,通常使用信号灯。
(2)使用共享内存的步骤:
1.调用 shmget()创建共享内存段 id。
2.调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间。3.访问加入到进程的那部分映射后地址空间,可用 IO 操作读写。
常用函数:
函数原型:int shmget(key_t key, size_t size, int shmflg)
功能:创建共享内存
头文件:#include <sys/ipc.h>
#include <sys/shm.h>
参数 key:由 ftok 生成的 key 标识,标识系统的唯一 IPC 资源
参数 size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是 4k 字节,
为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
参数 shmflg:如果要创建新的共享内存,需要使用 IPC_CREAT,IPC_EXCL,
如果是已经存在的,可以使用 IPC_CREAT 或直接传
返回值:成功时返回一个新建或已经存在的的共享内存标识符,取决于 shmflg 的参数。
失败返回-1 并设置错误码
函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:挂接共享内存
头文件:#include <sys/types.h>
#include <sys/shm.h>
参数 int shmid:共享内存的标识符,也就是 shmget 函数的返回值
参数 const void *shmaddr:映射到的地址,一般写 NULL,NULL 为系统自动帮我们完成映射
参数 int shmflg:通常为 0,表示共享内存可读可写,或者为 SHM_RDONLY,表示共享内存可读可写
返回值:成功返回共享内存映射到进程中的地址,失败返回-1
函数原型:int shmdt(const void *shmaddr);
功能:去关联共享内存
头文件:#include <sys/types.h>
#include <sys/shm.h>
参数 const void *shmaddr:共享内存映射后的地址
返回值:成功返回 0,失败返回-1
注意:shmdt 函数是将进程中的地址映射删除,
也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离,并不会删除内核里面的共享内存对象。
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:销毁共享内存
头文件:#include <sys/ipc.h>
#include <sys/shm.h>
参数 int shmid:要删除的共享内存的标识符
参数 cmd:IPC_STAT (获取对象属性) IPC_SET (设置对象属性) IPC_RMID(删除对象)
参数 struct shmid_ds *buf:指定 IPC_STAT (获取对象属性) IPC_SET (设置对象属性) 时用来保存或者设置的属性
(3)实验代码
/*在程序中,父子进程通过共享内存通信。*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int shmid;
key_t key;
pid_t pid;
char *s_addr, *p_addr;
key = ftok("./a.c", 'a');
shmid = shmget(key, 1024, 0777 | IPC_CREAT);
if (shmid < 0)
{
printf("shmget is error\n");
return -1;
}
printf("shmget is ok and shmid is %d\n", shmid);
pid = fork();
if (pid > 0)//父进程
{
p_addr = shmat(shmid, NULL, 0);
strncpy(p_addr, "hello-morris", 12);
wait(NULL);
exit(0);
}
if (pid == 0)//子进程
{
sleep(2);
s_addr = shmat(shmid, NULL, 0);
printf("s_addr is %s\n", s_addr);
exit(0);
}
return 0;
}
运行结果:
查看共享内存命令:ipcs -m
删除共享内存的命令:ipcrm -m [id]
3.2 消息队列
(1)本质上是一个队列,队列可以理解为内核维护的一个FIFO
(2)工作时A和B两个进程进行通信,A向队列中放入消息,B从队列中读出消息。
(3)消息队列使用步骤:1.创建key;2.msgget()通过key创建或打开消息队列对象id;3.使用msgsnd()/msgrcv()进行收发4.通过msgctl()删除ipc对象
函数原型:int msgget(key_t key, int msgflg)
功能:获取 IPC 对象唯一标识 i
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数 key_t key:和消息队列相关的 key
参数 int msgflg:访问权限
返回值:成功返回消息队列的 ID,失败返回-1
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:发送数据
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数 int msqid:消息队列 ID
参数 const void *msgp:指向消息类型的指针
参数 size_t msgsz:发送的消息的字节数。
参数 int msgflg:如果为 0,直到发送完成函数才返回,即阻塞发送
IPC_NOWAIT:消息没有发送完成, 函数也会返回,即非阻塞发
返回值:成功返回 0,失败返回-1
函数原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
功能:接收消息
参数 msqid:IPC 对象对应的 id
参数 msgp:消息指针,消息包含类型和字段
参数 msgsz:消息里的字段大小
参数 msgtyp:消息里的类型
参数 msgflg:位掩码,不止一个
返回值:成功返回接收到的字段大小,错误返回-1
函数原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf)
功能:控制操作,删除消息队列对象等
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数 int msqid:消息队列的 ID
参数 int cmd:IPC_STAT:读取消息队列的属性,然后把它保存在 buf 指向的缓冲区。
IPC_SET:设置消息队列的属性,这个值取自 buf 参数
IPC_RMID:
参数 struct msqid_ds *buf:消息队列的缓冲区
返回值:成功返回 0,失败返回-1
实验代码:
/*a.c 向消息队列里面写*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(void)
{
int msgid;
key_t key;
struct msgbuf msg;
//获取 key 值
key = ftok("./a.c", 'a');
//获取到 id 后即可使用消息队列访问 IPC 对象
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid < 0)
{
printf("msgget is error\n");
return -1;
}
printf("msgget is ok and msgid is %d \n", msgid);
msg.mtype = 1;
strncpy(msg.mtext, "hello", 5);
//发送数据
msgsnd(msgid, &msg, strlen(msg.mtext), 0);
return 0;
}
/*b.c 从消息队列里面读*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(void)
{
int msgid;
key_t key;
struct msgbuf msg;
key = ftok("./a.c", 'a');
//获取到 id 后即可使用消息队列访问 IPC 对象
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid < 0)
{
printf("msgget is error\n");
return -1;
}
printf("msgget is ok and msgid is %d \n", msgid);
//接收数据
msgrcv(msgid, (void *)&msg, 128, 0, 0);
printf("msg.mtype is %ld \n", msg.mtype);
printf("msg.mtext is %s \n", msg.mtext);
return 0;
}
查看共享队列命令:ipcs -q
3.3信号量(信号灯)
(1)实质就是个计数器(可理解为int a);通过计数值来提供互斥与同步。
注意请不要把信号量与之前所说的信号混淆起来,信号与信号量是不同的两种事物。让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
信号量只能进行两种操作等待和发送信号,即 P(sv)和 V(sv),他们的行为是这样的。举个例子,就是两个进程共享信号量 sv,一旦其中一个进程执行了 P(sv)操作,它将得到信号量,并可 以进入临界区,使 sv 减 1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行 V(sv)释放信号量,这时第二个进程就可以恢复执行。
信号灯也叫信号量,它能够用来同步进程的动作,不能传输数据。它的应用场景就像红绿灯,控制各进程使用共享资源的顺序。
(2)使用 System V 信号灯的步骤如下: 1.使用 semget()创建或打开一个信号灯集。 2.使用 semctl()初始化信号灯集。 3.使用 semop()操作信号灯值,即进行 P/V 操作。 P 操作:申请资源,申请完后信号灯值-1; V 操作:释放资源,释放资源后信号灯值+1
函数原型: int semget(key_t key, int nsems, int semflg)
功能 创建一个新信号量或取得一个已有信号量
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
参数 key_t key:信号量的键值
参数 int nsems:信号量的数量
参数 int semflg:标识
返回值:成功返回信号量的 ID,失败返回-1
函数原型: int semctl(int semid, int semnum, int cmd, union semun arg)
功能 :初始化信号灯集合
头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
参数 int semid:信号量 ID
参数 int semnum:信号量编号
参数 cmd:IPC_STAT(获取信号量的属性)
IPC_SET(设置信号量的属性)
IPC_RMID (删除信号量)
SETVAL(设置信号量的值)
参数 arg:union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
}
函数原型: int semop(int semid, struct sembuf *sops, size_t nsops)
功能:在信号量上执行一个或多个操作。
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
参数 int semid:信号量 ID
参数 struct sembuf *sops:信号量结构体数组
参数 size_t nsops:要操作信号量的
struct sembuf{
unsigned short sem_num; //要操作的信号量的编号
short sem_op; //P/V 操作,1 为 V 操作,释放资源。-1 为 P操作,分配资源。0 为等待,直到信号量的值变成 0
short sem_flg; //0 表示阻塞,IPC_NOWAIT 表示非阻塞
}
实验代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun
{
int val;
};
int main(void)
{
int semid;
int key;
pid_t pid;
struct sembuf sem;
union semun semun_union;
key = ftok("./a.c", 0666);
semid = semget(key, 1, 0666 | IPC_CREAT);
semun_union.val = 0;
semctl(semid, 0, SETVAL, semun_union);
pid = fork();
if (pid > 0)
{
sem.sem_num = 0;
sem.sem_op = -1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
printf("This is parents\n");
sem.sem_num = 0;
sem.sem_op = 1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
}
if (pid == 0)
{
sleep(2);
sem.sem_num = 0;
sem.sem_op = 1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
printf("This is son\n");
}
return 0;
}
4.信号通信
(1)信号的产生有三种方式:
- 由硬件产生,如从键盘输入 Ctrl+C 可以终止当前进程
- 由其他进程发送,如可在 shell 进程下,使用命令 kill -信号标号 PID,向指定进程发送信号。
- 异常,进程异常时会发送信号
在 Ubuntu 终端输入 kill -l,查看所有信号
(2)几个常用的函数
函数原型:int kill(pid_t pid, int sig);
头文件:用于向任何进程组或进程发送信号
参数 pid:大于 0,时为向 PID 为 pid 的进程发送信号
等于 0,向同一个进程组的进程发送信号;
等于-1,除发送进程自身外,向所有进程 ID 大于 1 的进程发送信号。
小于-1,向组 ID 等于该 pid 绝对值的进程组内所有进程发送信号。
参数 sig:设置发送的信号;
等于 0 时为空信号,无信号发送。常用来进行错误检查
返回值:执行成功时,返回值为 0;错误时,返回-1,并设置相应的错误代码 errno
函数原型:int raise(int sig);
功能:向进程自身发送信号,相当于 kill(getpid(),sig)
头文件:#include <signal.h>
参数 sig:信号
函数原型:unsigned int alarm(unsigned int seconds);
功能:设定的时间超过后产生 SIGALARM 信号,默认动作是终止进程
头文件:#include <unistd.h>
参数:设定的时间
注意:每个进程只能有一个 alarm()函数,时间到后要想再次使用要重新注册。