进程间的通信方式
无名管道 pipe
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
创建管道函数
#include <unistd.h>
int pipe(int fd[2]);
单个进程中的管道几乎没有任何用处。通常,进程会先调用 pipe,接着调用 fork,从而创建从父进程到子进程的IPC通道
管道读写
fork之后做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关 闭管道的读端(fd[0]),子进程关闭写端(fd[1])
示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
pid_t pid;
char buff[16];
//创建管道
if(pipe(fd) < 0){
perror("fail pipe create :");
return -1;
}
//创建子进程
if((pid = fork()) < 0){
perror("fail fork: ");
return -1;
}
else if (pid == 0){
//子进程关闭写端,开始接受父进程消息
close(fd[1]);
read(fd[0], buff, 15);
fputs(buff, stdout);
}
else {
//父进程关闭读端,开始写入信息
close(fd[0]);
write(fd[1] ,"Hello Pipe!\n" ,16);
}
return 0;
}
运行结果
命名管道 FIFO
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信
创建FIFO
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
返回值:成功返回0; 失败返回-1,可以通过errno判断错误类型
-
pathname:要创建的FIFO文件名且该文件必须不存在
-
mode:指定FIFO文件的权限(8进制数,如0664)
打开FIFO
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。
int fd;
fd = open(FIFO , O_WRONLY)
FIFO文件
管道文件读写
写入管道消息
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO "file"
int main()
{
//创建管道,不提醒已经存在的错误
if(mkfifo(FIFO,0600) < 0 && errno != EEXIST)
{
perror("FIFO make:");
return -1;
}
int fd;
int cnt=10;
//以只读的权限打开管道文件
if((fd = open(FIFO , O_WRONLY)) < 0)
{
perror("open FIFO:");
return -1;
}
//多次循环写入内容
while(cnt--)
{
write(fd , "hello\n" ,16);
sleep(1);
}
close(fd);
return 0;
}
读取管道消息
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#define FIFO "file"
int main()
{
//创建管道文件,如果已经存在不提示
if(mkfifo(FIFO , 0600) == -1) {
if(errno == EEXIST){
printf("FIFO already have\n");
}
else {
perror("mkfifo :");
return -1;
}
}
else{
printf("FIFO successfully!\n");
}
char buff[16];
int nread=0;
//打开管道文件
int fd = open(FIFO, O_RDONLY);
while(1)
{
//循环读取,输出到终端
nread = read(fd, buff, 15);
fputs(buff,stdout);
}
close(fd);
return 0;
}
运行结果
消息队列 MessageQueue
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
消息队列的数据格式
无论是发送还是接收消息,都有固定的格式。
struct msgstru
{
long msgtype; //消息类型
// 消息内容
};
消息结构体中,首位必须是 (long类型)消息类型 , 内容可以按需求定义,可以是数组,也可以是结构体
//文本类型
struct msgstru{
long msgtype;
char text[20];
};
//整型
struct msgstru{
long msgtype;
int start;
};
//结构体
typedef struct {
char name[20];
int age;
}Person;
struct msgstru{
long msgtype;
Person person;
};
创建和获取 ipc 内核对象
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- 参数key:相当于文件系统的文件名
- 参数msgflg:是一个操作指令
当想要创建一个消息队列时可以填IPC_CREAT | 0644,前面指的是创建的指令,后面的0644指的是消息队列的执行权限。 - 返回值:ipc 内核对象 id
key值可以使用 ftok() 函数获取
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 参数pathname:指定的文件名,这个文件必须是存在的而且可以访问的。
- id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,否则返回-1。
在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。
发送数据
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
-
参数 msqid:ipc 内核对象 id
-
参数 msgp:消息数据地址
-
参数 msgsz:消息正文部分的大小(不包含消息类型)
-
参数 msgflg:可选项
该值为 0:如果消息队列空间不够,msgsnd 会阻塞。
IPC_NOWAIT:直接返回,如果空间不够,会设置 errno 为 EAGIN. -
返回值:0 表示成功,-1 失败并设置 errno。
接收数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
- 参数 msqid:ipc 内核对象 id
- 参数 msgp:用来接收消息数据地址
- 参数 msgsz:消息正文部分的大小(不包含消息类型)
- 参数 msgtyp:指定获取哪种类型的消息
- 参数 msgflg:
如果为 0 表示没有消息就阻塞。
IPC_NOWAIT:如果指定类型的消息不存在就立即返回,同时设置 errno 为 ENOMSG
MSG_EXCEPT:仅用于 msgtyp > 0 的情况。表示获取类型不为 msgtyp 的消息
MSG_NOERROR:如果消息数据正文内容大于 msgsz,就将消息数据截断为 msgsz - msgrcv 函数从消息队列取出消息后,并将其从消息队列里删除。
获取和设置消息队列的属性
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid:消息队列标识符
cmd:控制指令
IPC_STAT:获得msgid的消息队列头数据到buf中
IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
buf:消息队列管理结构体。
返回值:
成功:0
出错:-1,错误原因存于error中
示例代码
接收
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
struct msgstru
{
long msgtype;
char msgtext[64];
};
int main()
{
key_t key;
struct msgstru msgr;
struct msgstru msgs;
int msgPid;
int nrecv=0;
key = ftok(".",1);
if((msgPid = msgget(key,IPC_CREAT|0666)) < 0)
{
perror("msgGet:");
return -1;
}
printf("MSGKEY : %x\n",key);
msgs.msgtype = 999;
strcpy(msgs.msgtext, "msgrcv send message!");
while(1){
nrecv = msgrcv(msgPid, &msgr, sizeof(msgr.msgtext), 666, 0);
printf("recv type : %ld\nmsg recv: %s, total %d bytes\n",msgr.msgtype, msgr.msgtext,nrecv);
msgsnd(msgPid, &msgs, strlen(msgs.msgtext), 0);
}
msgctl(msgPid, IPC_RMID, 0);
return 0;
}
发送
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
struct msgstru
{
long msgtype;
char msgtext[32];
};
int main()
{
key_t key;
int msgqid;
int nsend;
struct msgstru msgs;
struct msgstru msgr;
key = ftok(".",1);
if((msgqid = msgget(key, IPC_CREAT|0666)) < 0)
{
perror("msgGet:");
return -1;
}
msgs.msgtype = 666;
strcpy(msgs.msgtext, "msg send message!");
for(;;)
{
nsend = msgsnd(msgqid, &msgs, strlen(msgs.msgtext), 0);
msgrcv(msgqid, &msgr, sizeof(msgr.msgtext), 999, 0);
printf("message type: %ld\ntext: %s\n",msgr.msgtype, msgr.msgtext);
sleep(1);
}
msgctl(msgqid, IPC_RMID, 0);
return 0;
}
运行结果
共享存储 SharedMemory
共享内存(Shared Memory)就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变。
共享内存并未提供锁机制,也就是说,在某一个进程对共享内存的进行读写的时候,不会阻止其它的进程对它的读写。如果要对共享内存的读/写加锁,可以使用信号灯。
获取或创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
-
参数key 是共享内存的键值,是一个整数,typedef unsigned int key_t,是共享内存在系统中的编号,不同共享内存的编号不能相同,这一点由程序员保证。key用十六进制表示比较好。
-
参数size 是待创建的共享内存的大小,以字节为单位。
-
参数shmflg 是共享内存的访问权限,与文件的权限一样,0666|IPC_CREAT表示全部用户对它可读写,如果共享内存不存在,就创建一个共享内存。
连接共享内存
void *shmat(int shm_id, const void *shm_addr, int shmflg);
-
参数shm_id 是由shmget函数返回的共享内存标识。
-
参数shm_addr 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
-
参数shm_flg 是一组标志位,通常为0。
-
成功时返回共享内存的地址,失败返回-1
断开共享内存
int shmdt(const void *shmaddr);
-
参数shmaddr 共享内存的地址。
-
调用成功时返回0,失败时返回-1.
删除共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
-
参数shm_id 是共享内存标识符。
-
参数command IPC_RMID代表删除,还有其他参数。
-
参数buf 可以选择默认,0。
示例代码
写入
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define SIZE 1024
int main()
{
int shmid;
char *shmaddr;
char buff[SIZE];
key_t KEY;
//获取 KEY 值
KEY = ftok(".",1);
//创建共享内存,得到共享内存的标识符
if((shmid = shmget(KEY, SIZE, 0600|IPC_CREAT)) < 0){
perror("shmget:");
return -1;
}
//挂载共享内存,获取该地址
shmaddr = (char *)shmat(shmid, 0, 0);
//循环写入
printf("写入消息到共享内存中(输入 quit 退出):\n");
while(1)
{
fgets(buff, SIZE, stdin);
strcpy(shmaddr,buff);
if(strcmp(buff,"quit\n") == 0){
break;
}
memset(buff, '\0', SIZE);
}
printf("进程退出\n");
//卸载
shmdt(shmaddr);
//删除
shmctl(shmid, IPC_RMID, 0);
return 0;
}
读取
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define SIZE 1024
int main()
{
int shmid;
char *shmaddr;
char buff[SIZE];
key_t KEY;
//获取 KEY 值
KEY = ftok(".",1);
//获取共享内存,得到共享内存的标识符
if((shmid = shmget(KEY, SIZE, 0)) < 0){
perror("shmget:");
return -1;
}
//挂载共享内存,获取该地址
shmaddr = (char *)shmat(shmid, 0, 0);
printf("开始接收共享内存的消息 \n");
//循环读取,判断并打印
while(strcmp(buff, "quit\n") != 0)
{
if(strcmp(shmaddr, buff) != 0 && strlen(shmaddr) > 0){
printf("get shm message: %s", shmaddr);
strcpy(buff, shmaddr);
}
}
//卸载共享内存
shmdt(shmaddr);
printf("进程退出\n");
return 0;
}
运行结果
共享内存相关命令
查看
ipcs -m
删除
ipcrm -m <shmid>
进程间信号量
信号量可以用来进程间的同步
在共享内存中,没有进行同步互斥的功能,而信号量的功能就是实现同步和互斥
在共享内存中的示例代码中,可以看出有明显的不足,关于消息的循环读取会严重的占用CUP,这时候就需要信号量来同步消息的发送和接收
进程间的信号量与线程的信号量的使用方法类似,进程间的称为 <有名信号量>
创建有名信号量
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
-
参数name 信号量的名字,两个不同的进程通过同一个名字来进行信号量的传递。
-
参数oflag 当他是O_CREAT时,如果name给出的信号量不存在就创建,此时必须给出mode和vaule。当O_CREAT和O_EXCL同时存在时,如果已存在该信号量,就报错。0为默认。
-
参数mode 指定信号量的权限。
-
参数vaule 信号量的初始值。
-
返回值sem_t 是一个结构,如果函数调用成功,则返回指向这个结构的指针,里面装着当前信号量的资源数,如果失败则返回SEM_FAILED。
创建的信号量默认放在 /dev/shm/ 目录下
信号量操作
P操作
int sem_wait(sem_t *sem);
如果有可用的资源(信号量>0),那么占用一个资源(信号量-1)。如果没有可用的资源(信号量=0),则进程被阻塞,直到系统重新给他分配资源。
V操作
sem_post(sem_t *sem)
如果在该信号量的等待队列中有进程在等待该资源,则唤醒一个进程,否则释放一个资源(信号量+1)
进程间的信号量的P/V操作与线程中的使用方法类似
关闭有名信号量
#include <semaphore.h>
int sem_close(sem_t *sem);
与线程中不同,进程间需要先关闭信号量,再删除
删除有名信号量
#include <semaphore.h>
int sem_unlink(const char *name);
当使用完有名信号后,需要调用函数sem_unlink来释放资源。
示例代码
在上文中共享内存中加入信号量
写入
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <semaphore.h>
#include <errno.h>
#include <fcntl.h>
#define SIZE 1024
#define SEM_NAME "sem_text"
int main()
{
int shmid;
char *shmaddr;
char buff[SIZE];
key_t KEY;
sem_t *semid;
//创建有名信号量
if((semid = sem_open(SEM_NAME, O_CREAT, 0666, 0)) == SEM_FAILED){
perror("sem_open");
return -1;
}
KEY = ftok(".",1);
if((shmid = shmget(KEY, SIZE, 0600|IPC_CREAT)) < 0){
perror("shmget:");
return -1;
}
shmaddr = (char *)shmat(shmid, 0, 0);
printf("写入消息到共享内存中(输入 quit 退出):\n");
while(strcmp(buff, "quit\n") != 0)
{
memset(buff, '\0', SIZE);
fgets(buff, SIZE, stdin);
strcpy(shmaddr,buff);
//将内存写到共享内存后进行V操作,开始同步读的进程
sem_post(semid);
}
printf("进程退出\n");
//关闭并销毁信号量
sem_close(semid);
sem_unlink(SEM_NAME);
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, 0);
return 0;
}
读取
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <semaphore.h>
#define SIZE 1024
#define SEM_NAME "sem_text"
int main()
{
int shmid;
char *shmaddr;
char buff[SIZE];
key_t KEY;
sem_t *semid;
//获取有名信号量的地址
if((semid = sem_open(SEM_NAME, 0)) == SEM_FAILED){
perror("sem_open:");
return -1;
}
KEY = ftok(".",1);
if((shmid = shmget(KEY, SIZE, 0)) < 0){
perror("shmget:");
return -1;
}
shmaddr = (char *)shmat(shmid, 0, 0);
printf("开始接收共享内存的消息 \n");
while(strcmp(buff, "quit\n") != 0)
{
//P操作,接收同步信号
sem_wait(semid);
printf("get shm message: %s", shmaddr);
strcpy(buff, shmaddr);
}
//关闭信号量
sem_close(semid);
shmdt(shmaddr);
printf("进程退出\n");
return 0;
}
运行结果
当程序运行时,可以在 /dev/shm/ 目录下看到该信号量
在程序推出后,该信号量被销毁