每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程间要交换数据必须经过内核,在内核开辟一段缓冲区,进程1把数据拷到内核缓冲区,进程2 从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
管道(pipe)
管道是最基本的IPC机制,由pipe函数创建:
#include<unistd.h>
int pipe(int filedes[2]);
pipe 在内核中开辟一块缓冲区(管道)通信用。filedes[0]->读端,filedes[1]->写端。向这个文件读写就是在读写缓冲区。pipe调用成功返回0,否则返回-1。
进程间通信步骤:
1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
->使用管道的限制:
- 两个进程通过一个管道只能实现单向通信。
- 管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从公共祖先那里继承管道文件描述符——管道的通信需要进程之间有关系。
->注意一下四中特殊情况:
- 写端的文件描述符关闭(管道写端引用计数为 0),读端仍然读取数据,当管道中剩下的数据被读完,read会返回0,就像读到文件末尾。
- 写端没有关闭,但是不写数据,读端读完数据,会阻塞,直到管道中有数据可读,才读到数据并返回。
- 读端关闭,有进程向写端write,该进程会收到SIGPIPE,通常会导致进程异常终止。
4.读端没有关闭,也没有读数据,写数据,管道被写满时write会阻塞,直到管道中有位置才写入数据并返回。
命名管道(FIFO)
->fifo不同于pipe之处在于:它提供一个路径名与之关联,以FIFO的文件形式存储与文件系统中。
->命名管道是一个设备文件,即使进程和创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
ps:FIFO总是按照先进先出的原则,先写入的数据先被读出来。
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char* path,mode_t mod,dev_t dev);
int mkfifo(const char* path,mode_t mod);
mknod函数中,path为创建命名管道的全路径名:mod为创建命名管道的模式,指明它的存取权限:dev为设备值,取决于文件创建的种类,只在创建设备文件的时候才会用。
ps:两个函数都是调用成功返回0;否则返回-1。
eg:
umask(0);
if(mknod("/tmp/fifo",S_FIFO | 0666) == -1)
{
perror("mkfifo error\n");
exit(1);
}
函数mkfifo 前两个参数和mknod一样,下面是mkfifo的代码:
umask(0);
if(mkfifo("/tmp/fifo",S_FIFO | 0666) == -1)
{
perror("mkfifo error\n");
exit(1);
}
->S_FIFO | 0666 指明创建一个命名管道且存取权限为0666,即 创建者,创建者组,其他用户 对该命名管道的访问权限都是读写。
->使用命名管道的时候,一定要先有open函数将其打开,因为命名管道存在于硬盘上,管道是内存中的特殊的文件。
->ps:调用open()打开命名管道进程可能被阻塞;同时打开读写(O_RDWR)则一定不会阻塞;若只读(O_RDONLY)打开,则会阻塞到有写方式打开的管道;若只写(O_WRONLY)打开,则会阻塞到有读的方式打开管道。
->文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标志一个IPC的通道。
->命名管道也称为FIFO文件,一个特殊类型的文件,在文件系统中以文件名的形式存在,但行为与管道相似;
->因为Linux中所有的事物都可以被认为是文件,所以对命名管道的操作就像文件操作一样。
IPC(一)消息队列
消息队列提供了一种 从一个进程向另一个进程发送一个数据快的方法。
与管道的不同:管道式基于字节流的,消息队列是基于消息的。
消息队列的读取不一定是先入先出。
与命名管道有一样的不足:
- 每个消息的最大长度有上限(MSGMAX)
- 每个消息的总的字节数有上限(MSGMNB)
- 系统上消息队列的总数也是有限的(MSGMNI)
->IPC 对象数据结构(/usr/include/linux/ipc.h)
消息队列,信号量,共享内存都有这样的数据结构。
->消息队列结构(/sur/include/linux/msg.h)
第一个条目就是IPC结构体;后面的是消息队列所私有的,消息队列是同链表实现的。
->函数
- 创建新消息队列或取得已存在消息队列
原型:int msgget(key_t key,int msgflg);
参数:
key:可以认为是一个端口号,也可以由函数ftok生成;
msgflg:创建方式
IPC_CREAT:如果IPC不存在,则创建一个IPC资源,否则打开操作。
IPC_EXCL:只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
如果单独使⽤用IPC_CREAT,XXXget()函数要么返回⼀一个已经存在的共享内存的操作符,要
么返回⼀一个新建的共享内存的标识符。
如果将IPC_CREAT和IPC_EXCL标志一起使用,XXXget()将返回一个新建的IPC标识符;如果该IPC资源已存在,或者返回-1。
IPC_EXEL标志本⾝身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。 - 向队列读/写消息
msgrcv从队列中取⽤用消息:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgsnd将数据放到消息队列中:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); - 设置队列属性
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
msgctl 系统调用对 msgqid 标识的消息队列执行 cmd 操作,系统定义了 3 种 cmd 操作:
IPC_STAT , IPC_SET , IPC_RMID
IPC_STAT : 该命令用来获取消息队列对应的 msqid_ds 数据结构,并将其保存到 buf 指定的地址空间。
IPC_SET : 该命令用来设置消息队列的属性,要设置的属性存储在buf中。
IPC_RMID : 从内核中删除 msqid 标识的消息队列。
IPC(二) 信号量
->信号量的本质是⼀一种数据操作锁,它本⾝身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
->当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用。
->大于0,资源可以请求,等于0,无资源可用,进程会进入睡眠状态直至资源可用。
->当进程不再使用一个信号量控制的共享资源时,信号量的值+1,对信号量的值进行的增减操作均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。
->但在信号量的创建及初始化上,不能保证操作均为原子性。
-?为什么要使用信号量
->为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题。
-?信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
IPC(三)共享内存
->共享内存比管道高效的原因:
(1)用管道通信,写数据时,它要有自己的一个buf,先把自己的数据保存在用户区;
(2)在进行进程间通信时,它需要调用系统的read和write函数,把数据从用户区拷贝到内核
(3)读的那一方,又要把数据从内核区拷贝到自己的用户区
->所以用管道通信,它有两次的数据拷贝,而共享内存不需要任何的数据拷贝,直接使用共享内存即可。共享内存高效,但内部未提供同步与互斥机制,导致通信时,会相互干扰,所以经常与信号量配合使用。
->共享内存可以实现进程间通信,server.c与client.c就需要一些公共的接口:
int create_shm();//创建共享内存
int get_shm();//获取共享内存
int destroy_shm();//删除共享内存
void* attach();//挂接共享内存
int detach();//取消挂接(分离)
ps:
-?为什么进程间需要通信?
1、数据传输
一个进程需要将它的数据发送给另一个进程。
2、资源共享
多个进程之间共享同样的资源。
3、通知事件
一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
4、进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
->Linux进程间通信(IPC)由以下几部分发展而来:
1、UNIX进程间通信
2、基于System V进程间通信
3、POSIX进程间通信
->linux下进程间通信的⼏几种主要⼿手段简介:
1 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
2 信号(Signal):信号是⽐比较复杂的通信⽅方式,⽤用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本⾝身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统⼀一对外接口,用sigaction函数重新实现了signal函数);
3 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使⽤用,来达到进程间的同步及互斥。
5 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
6 套接⼜⼝口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在⼀一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。