参考链接:
1. 进程间通信及使用场景
2. 进程间通信机制IPC
3. 看图理解进程间通信IPC==重点
4. 进程间通信和同步
1. 介绍
在linux下有多中进程间的通信方法:
- 半双工通道 PIPE
- 命名管道 FIFO
- 消息队列
- 信号量
- 共享内存
- socket
2. 无名管道(PIPE)
应用
- 在shell中使用,用于重定向
- 用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。
注意事项 - 只能用于具有亲缘关系的进程之间的通信(父子进程或者兄弟进程)
- 半双工通信模式,具有固定的读端和写端
- 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read(),write()等函数.但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中.
int fd[2];
//定义
int* write_fd = &fd[1];
int* read_fd = &fd[0];
//创建
ret = pipe(fd);
//读写
write(*write_fd,string,size);
read(*read_fd,buffer,sizeof(buffer));
//关系
close(*write_fd);
close(*read_fd);
管道的操作是阻塞性质的。
管道读写注意事项
- 只有在管道的读端存在时,向管道写入数据才有意义.否则,向管道写入数据的进程将收到内核传来的SIGPIPE信号(通常为Broken pipe错误).
- 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据.如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞.
- 父子进程在运行时,它们的先后次序并不能保证,因此,在为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用sleep()函数,当然这种调用不是很好的解决方法,在后面学到进程之间的同步与互斥机制之后。
3 .有名管道(FIFO)
FIFO有时被称为命名管道,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的祖先进程。但是,通过FIFO,不相关的进程之间也能交换数据。
FIFO用途:
- (1) shell命令使用FIFO将数据从一条管道传送到另一条管道,无需创建中间临时文件。
实例:考虑这样一个过程,他需要对一个输入文件进行两次处理,示意图如下 - (2) 客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。
创建FIFO:
#include <sys/stat.h>
int mkfifo(const char* path, mode_t mode);
int mkfifoat(int fd, const char* path, mode_t mode);
说明:
mkfifoat与mkfifo相似,像之前其他at系列函数一样,有3种情形:*
- (1) 如果path参数指定了绝对路径名,则fd被忽略,此时mkfifoat和mkfifo一样。
- (2) 如果path参数指定了相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。
- (3) 如果path参数指定了相对路径名,并且fd参数指定了AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似。
当我们使用mkfifo或者mkfifoat函数创建FIFO时,要用open打开,确是,正常的I/O函数(如close、read、write、unlink)都需要FIFO。当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生如下影响:
-
(1) 没有指定O_NONBLOCK时,只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读而打开这个FIFO为止。
-
(2) 如果指定了P_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开这个FIFO,那么只写open将返回-1,并将errno设置为ENXIO。
一个给定的FIFO有多个写进程是很常见的,这就意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。
实例:
-
考虑这样一个过程,他需要对一个输入文件进行两次处理,示意图如下:
image.png
我们可以使用FIFO和tee命令如下处理:
mkfifo fifo1
prog3 < fifo1 &
prog1 < (输入文件) | tee fifo1 | prog2
执行流程如下:
image.png-
有一个服务器进程,它与很多客户进程相关,每个客户进程都可将请求写到一个该服务器进程创建的FIFO中。由于该FIFO有多个写进程,因此客户进程每次发送给服务器的数据长度要小于PIPE_BUF字节,这样就能避免客户进程之间的写交叉。
image.png
但是这种类型的FIFO设计有问题,服务器如何回应各个客户进程呢?
- 一种解决方法是,每个客户进程都在其请求中包含它的进程ID,然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础的。
- 例如,服务进程可以用名字/tmp/serv1.XXXXX创建FIFO,其中XXXXX被替换成客户进程的进程ID,如下图所示: image.png
4. 消息队列
linux进程间通信-消息队列
优点:消息队列与管道、FIFO相比,具有更大的灵活性
- 提供格式字节流,减少开发人员的工作量
- 消息具有类型,在实际应用中,可作为优先级使用
- 消息队列可以在几个进程间复用,不用管这几个进程是否具备亲缘关系
- 较为高效,不像共享内存一样还要自己处理竞争条件和临界代码区。
缺点:
- 容量有限制,所能容纳的字节数
- 每个消息队列所能容纳的最大消息数有限制
4.1 有关消息队列的相关函数
-
int msgget(key_t key, int flag);
msgget用于创建一个新队列或打开一个现有队列。 -
int msgsnd(int msqid, const void * ptr, size_t nbytes, int flag);
msgsnd将新消息添加到队列尾端。每一个消息包含一个正的长整形类型的字段、一个非负的长度以及实际数据字节数,所有这些都在将消息添加到队列时,传送给msgsnd。 -
int msgctl(int msqid, int cmd, struct msqid_ds * buf);
对队列执行cmd操作,例如:IPC_STAT(读取消息)
、IPC_SET(设置消息)
、IPC_RMID(删除消息)
-
ssize_t msgrcv(int msqid, void * ptr, size_t nbytes, long type,int flag);
msgrcv用于从队列中取消息,我们不一定要已先进先出次序取消息,也可以按照消息的类型字段取消息。 -
key_t ftok(char* pathname ,char proj)
返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作。
key = ftok(path_ptr,'a');
ipc_id = ipc(MSGGET,(int)key,flags,0,NULL,0);
4.2 消息队列基础理论
消息队列就是一个消息的链表,每个消息队列都有一个队列头,用结构体struct msg_queue
来描述。队列头中包含了该消息队列的大量信息,包括消息队列的键值、用户ID、组ID、消息队列中的消息数目等。
struct kern_ipc_perm{
//内核中记录消息队列的全局数据结构 msg_ids能够访问到该结构
key_t key; //该键值则唯一对应一个消息队列
key_t uid;
key_t gid;
key_t cuid;
key_t cgid;
mode_t mode;
unsigned long seq;
}
读写操作
消息读写操作非常简单,每个消息队列都有如下的数据结构:
struct msgbuf{
long mtype;
char mtext[1];
}
- mtype 代表消息类型,读取消息的依据。
- mtext 代表消息内容。
获得或者设置消息队列的属性
- msgctl(msgid,IPC_STAT,struct msqid_ds* Rbuf) 获得属性
- msgctl(msgid,IPC_SET,struct msqid_ds* Wbuf) 设置属性
完整示例代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/msg.h>
#include <unistd.h>
void msg_stat(int ,struct msqid_ds);
int main(int argc, char const *argv[])
{
int gflags,sflags,rflags;
key_t key;
int msgid;
int reval;
struct msgbuf
{
int mtype;
char mtext[1];
}msg_sbuf;
struct msgbuf
{
int mtype;
char mtext[10];
}msg_rbuf;
struct msqid_ds msg_ginfo,msg_sinfo;
char* msgpath = "/unix/msgqueue";
key = ftok(msgpath,'a');
gflags = IPC_CREAT | IPC_EXCL;
msgid = msgget(key,gflags | 00666);
if(msgid == -1)
{
printf("msg queue creart error\n");
return;
}
//创建消息队列后,输出消息队列的默认属性
msg_stat(msgid,msg_ginfo);
sflags = IPC_NOWAIT;
msg_sbuf.mtype = 10;
msg_sbuf.mtext[0] = 'a';
//send msg
reval = msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);
if(reval == -1)
{
printf("messsge send error\n");
}
//发送消息后,输出消息队列属性
msg_stat(msgid,msg_ginfo);
rflags = IPC_NOWAIT | MSG_NOERROR;
reval = msgrcv(msgid,&msg_rbuf,4,10,rflags);
if(reval == -1){
printf("read msg error\n");
}else{
printf("read from msg queue %d bytes\n",reval );
}
//从消息队列中读出消息后,输出消息队列的属性
msg_stat(msgid,msg_ginfo);
msg_sinfo.msg_perm.uid = 8;
msg_sinfo.msg_perm.gid =8;
msg_sinfo.msg_gbytes = 16388;
// 此处验证超级用户可以更改消息队列的默认 msg_qbytes
// 注意这里设置的值大于默认值
reval = msgctl(msgid,IPC_SET,&msg_sinfo);
if(reval == -1){
printf("msg set info error\n");
return;
}
msg_stat(msgid,msg_ginfo);
// 验证设置消息队列属性
reval = msgctl(msgid,IPC_RMID,NULL); //删除消息队列?
if(reval == -1){
printf("unlink msg queue error\n");
return;
}
}
void msg_stat(int msgid,struct msqid_ds msg_info)
{
int reval;
sleep(1);
reval = msgctl(msgid,IPC_STAT,&msg_info);
if(reval == -1){
printf("get msg info error\n");
return;
}
printf("\n");
printf("current number of bytes on queue is %d\n",msg_info.msg_cbytes );
printf("number of message in queue is :%d\n", msgid_info.msg_qnum );
printf("max number of bytes on queue is %d\n",msg_info.msg_qbytes );
//每个消息队列的容量都有限制 MSGMNB,值的大小因系统而异。在创建新的消息队列时,msg_qbytes的默认值就是MSGMNB。
printf("pid of last msgsnd is %d\n", msg_info.msg_ispid );
printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid );
printf("last msgsnd time is %s\n",ctime(&(msg_info.msg_stime)) );
printf("last msgrcv time is %s\n",ctime(&(msg_info.msg_rtime)) );
printf("last change time is %s\n",ctime(&(msg_info.msg_ctime)) );
printf("msg uid is %d\n", msg_info.msg_perm.uid );
printf("msg gid is %d\n", msg_info.msg_perm.gid);
}
5. 信号量
[高质量嵌入式编程 274页]
信号量主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时进程也可以修改该标志。用于访问控制及进程同步。
- 二值信号灯:类似于互斥锁。值为0 或1.
二值信号灯能够实现互斥锁的功能。信号灯强调共享资源,只要共享资源可用,其它进程同样可以修改信号灯的值。。互斥锁强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。 - 计算信号灯:值为任意非负值
使用信号灯:
- 打开或创建信号灯
- 信号灯操作,linux可以增加或减小信号灯的值,相当于对共享资源的释放和占有。
- 获得信号灯的属性
问题:
- 信号灯操作数目的限制,信号灯的最大数目,一个调用可操作的最大数,系统范围内的信号灯集数目都是有限制的。
- 竞争问题:在创建第一个信号灯的进程同时也初始化信号灯。解决办法为:
- 创建第一个信号灯的进程必须调用semop,这样sem_otime才能变为非零值。
- 因为第一个进程可能不调用semop,或者semop操作需要很长时间。第二个进程可能无限期等待。
实例:
【参看 高质量嵌入式linux c编程 278页】
6. 共享内存
- 最有用也是最快的进程间通信方式
- 效率高
- 管道和消息队列等通信方式,需要在内核和用户空间进行四次数据复制。共享内存复制两次。
6.1 mmap
- 头文件:
#include <unistd.h> #include <sys/mman.h>
void *mmap(void* start, size_t length,int prot,int flags,int fd,off_t offsize);
使用普通文件提供内存映射
fd = open(name,flag,mode);
if(fd<0)
ptr = mmap(NULL,len,PROT_READ |PROT_WRITE,MAP_SHARED,fd,0);
6.2 父子进程之间通过匿名映射实现共享
typedef struct {
char name[4];
int age;
}people;
people* p_map;
p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
为了更安全的通信,通常共享内存需要和信号灯等同步机制共同使用。
网友评论