1.什么是进程间的通信及进程间通信的目的
a.什么是进程间的通信
进程间通信(IPC,InterProcess Communication)
是指在不同进程之间传播或交换信息。
它的实质是让不同的进程看到相同的文件资源。
b.进程间通信的目的
数据传输
:一个进程需要将它的数据
发送给另一个进程资源共享
:多个进程之间共享同样的资源
通知事件
:一个进程需要向另一个或一组进程发送消息
,通知它发生了什么事(例如:一个进程退出时要通知它的父进程
)进程控制
:有些进程希望完全控制另一个进程
的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常
,并能够及时知道它的状态改变
。
2.进程间通信的分类
a.管道
- 匿名管道
pipe
- 命名管道
b.System V IPC
- System V
消息队列
- System V
共享内存
- System V
信号量
c.POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
3.管道
(1)匿名管道
a.什么是管道?
在使用命令时,我们经常可以用到管道,例如ps aux | grep test
,作用是把前面命令的输出当前后边命令的输入
。实际上,管道
是Unix操作系统的一种古老的进程间通信形式。
**我们把从一个进程连接到另一个进程的一个数据流称为一个管道。**下边这幅图可以形象的描述管道的功能:
b.匿名管道的使用
功能:创建一个匿名管道
参数:为文件描述符数组,fd[0]代表读端
,fd[1]代表写端
返回值:成功返回1,失败返回错误代码
上图我们可以发现管道的两端其实是两个进程,一个进程负责从管道中读,而另一个进程负责写到管道中,在读或写的过程中,每一个进程只能打开读端或者写端,不能同时打开两个。调用pipe函数时,首先在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,然后通过pipefd参数传出给用户进程两个文件描述符,pipefd[0]指向管道的读端,pipefd[1]指向管道的写段。在用户层面看来,打开管道就是打开了一个文件,通过read()或者write()向文件内读写数据,读写数据的实质也就是往内核缓冲区读写数据。
d.pipe匿名管道的两个实例
1.键盘读取数据写入管道,然后读取管道,将数据打印到显示屏上:代码位于
https://github.com/hansionz/Linux_Code/tree/master/test_pipe
2.fork子进程利用匿名管道实现两个进程间的通信,具体实现方法如下:
- 调用
pipe函数
,由父进程创建管道
,得到两个文件描述符指向管道的两端
- 父进程调用
fork创建子进程
,则对于子进程
,也有两个文件描述符指向管道的两端
子进程关闭读端
,只进行写操作
;父进程关闭写端
,只进行读操作
。数据从写段流入到读端,这样就形成了进程间通信
。
代码位于:
https://github.com/hansionz/Linux_Code/tree/master/test_pipe1
e.匿名管道的特点
- 只提供
单向通信
,也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2 就只能读取文件的内容。 - 只能用于具有
血缘关系的进程
间通信,通常用于父子进程建通信
- 管道是基于
字节流
来通信的 - 依赖于文件系统,它的生命周期
随进程
的结束结束(随进程)
- 其本身
自带同步互斥
效果
f.四种特殊情况
- 如果
写端
对应的文件描述符被关闭,则read返回0
- 如果有指向
管道写端的文件描述符没有关闭(管道写段的引用计数大于0)
,而持有管道写端的进程没有向管道内写入数据
,假如这时有进程从管道读端读数据
,那么读完管道内剩余的数据后就会阻塞等待
,直到有数据可读才读取数据并返回
。 - 如果
读端
对应的文件描述符被关闭,则write会产生SIGPIPE进而导致write进程退出
- 如果有指向管道
读端的文件描述符没有关闭(管道读端的引用计数大于0)
,而持有管道读端的进程没有从管道内读数据
,假如此时有进程通过管道写段写数据
,那么管道被写满后就会被阻塞
,直到管道内有空位置后才写入数据并返回
。
总结:谁快谁等待
(2)命名管道(FIFO)
虽然匿名管道可以实现两个进程间的通信,但是它也有一些缺点
:
- 只适用于具有
亲缘关系
的进程通信 - 只能
单向
通信
为了使任意两个进程
之间能够通信,就提出了命名管道(named pipe 或 FIFO)
a.命名管道的创建
- 命名管道可以通过
命令行
来创建,命令为:
$ mkfifo filename(文件名)
- 命名管道也可以在文件中
根据函数创建
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char* filename,mode_t mode);//mode为权限
//返回值:成功返回0,失败返回-1
例如:
int main()
{
mkfifo("myfifo",0664);
return 0;
}
b.命名管道与命名管道的区别
-
命名管道
使用mkfifo
创建,需要使用open
打开,匿名管道
使用pipe函数创建并打开
这是因为:命名管道是设备文件,它是存储在硬盘上的,而管道是存在内存中的特殊文件。但是需要注意的是,命名管道调用open()打开有可能会阻塞,但是如果以读写方式(O_RDWR)打开则一定不会阻塞;以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;如果以只写方式(O_WRONLY)打开时同样也会被阻塞,知道有以读方式打开该管道。 -
命名管道提供了一个
路径名与之关联
,以FIFO文件的形式存储于文件系统中
,能够实现任何两个进程之间通信
。而匿名管道对于文件系统是不可见
的,它仅限于在父子进程之间的通信
。 -
FIFO(first input first output)
总是遵循先进先出
的原则,即第一个进来的数据会第一个被读走
c.利用命名管道实现server/client
之间的通信
服务端
负责创建命名管道
,并向管道文件中写入数据
,客户端
负责打开管道文件
,并读取文件中的数据
。
代码位于
https://github.com/hansionz/Linux_Code/tree/master/test_mkfifo
4.消息队列
a.什么是消息队列
- 消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法
- 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。
- 我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
- 消息队列是地址空间中的内部链表。消息可以顺序地发送到队列中,并以几种不同的方式从队列中获取。当然,每个消息队列都是由 IPC标识符所唯一标识的。
b.创建消息队列
msgget函数
int msgget(key_t key,int msgflg);
功能:用来创建和访问一个消息队列
//key:表示消息队列的名字,它必须具有唯一性
//msgflg:用法和创建文件时mode模式标记是一样的
//IPC_CREAT|IPC_EXCL 创建消息队列如果已经存在则出错返回
//IPC_CREAT 尝试创建消息队列,没有则创建,已经存在则打开它
//返回值:成功返回一个非负整数,既该消息队列的标识码,失败返回-1
ftok函数
功能:ftok函数的作用是形成唯一的key值
msgctl函数
功能:控制消息队列的函数
int msgctl(int msgid,int cmd,struct msgid_ds *buf);
参数说明:
msgid:消息队列标识码
cmd:要采取的动作(IPC_RMID:删除消息队列)
返回值:失败返回-1,成功返回0
msgsnd
功能:把一条消息添加到消息队列中
int msgsnd(int msgid,const void *msgp,size_t msgsz,int msgflg);
参数说明:
msgid:消息队列标识码
msgp:是一个指针,指针指向准备发送的消息
msgsz:发送消息长度,不包括消息类型的long int 型
msgflg=IPC_NOWAIT表示队列满不等待,返回错误
- 消息结构
struct msgbuf{
long mtype;//类型
char mtext[];
}
//它必须以long int长整数开始,接受者函数利用这个厂整数确定消息的类型,它必须小于系统规定的上限值
msgrcv函数
功能:从一个消息队列接收消息
ssize_t msgrcv(int msgid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
参数说明:
msgid:消息队列的标识(区别其他消息队列)
msgp:是指向以上边的调用者定义的结构:
msgsz: 消息队列大小 msgsz=sizeof(struct msgbuf) - sizeof(long);
msgflg:
IPC_NOWAIT:立即返回,如果没有请求类型的消息在队列中。
MSG_EXCEPT:与msgtyp大于0用于在队列中与从msgtyp不同消息类型读取所述第一消息。
MSG_NOERROR:如果大于msgsz字节数进行截断。
c.消息队列实现进程间通信
代码位于:
https://github.com/hansionz/Linux_Code/tree/master/test_msgque
d.ipcs和ipcrm命令
ipcs -q
显示IPC资源ipcrm -q 消息队列的msgid
手动删除IPC资源ipc
资源随内核
5.共享内存
a.什么是共享内存
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。因此,采用共享内存的通信方式效率是非常高的。
共享内存的缺点:共享内存没有进行同步与互斥机制
b.创建共享内存
shmget函数
功能:创建共享内存
int shmget(key_t key, size_t size,int shmflg);
参数:
key:共享内存的名字
size:共享内存的大小(必须是页的整数倍,1页=4kb=4096字节)
shmflg:和创建文件是mode的使用一样
返回值:成功返回共享内存的标识码
shmat函数
void *shmat(int shmid,const void *shmaddr,int shmflg);
参数:
shmid:共享内存标识符
shmaddr:指定内存的地址
返回值:成功返回指向内向内存的指针
shmdt函数
功能:将共享内存段与当前进程脱离
int shmdt(const void *shmaddr);
参数:
shmaddr:由shmat所返回的指针
返回值:成功返回0,失败返回-1
//将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
参数:
shmid:共享内存标识码
cmd:将要采取的动作(IPC_RMID:删除共享内存段)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1
c.共享内存实现进程间通信
服务端创建共享内存并与其建立连接,客户端获得共享内存建立连接后向内存中写入数据,等待服务端接收数据。
代码位于:
https://github.com/hansionz/Linux_Code/tree/master/test_shm
d.ipcs&ipcrm
ipcs -m
查看共享内存资源ipcrm -m
删除挂接共享内存
注:nattch代表有几个进程和共享内存建立连接
6.信号量
a.什么是信号量
信号量机制是一种功能较强的机制,可以用来解决互斥与同步问题
,它只能被两个标准的原语wait(S)
和 signal(S)
来访问,也可以记为“P操作”和“V操作”
。
临界资源
:两个进程看到的一份公共资源称为“临界资源”,也就是说这些资源一次只允许一个进程使用,各个进程中访问“临界资源“的代码称为”临界区“。
互斥
:在临界区中,每个进程只能独占式、排他式的访问临界资源。
同步
:在互斥的基础上,让进程按照顺序公平的访问资源
信号量本质上是一个计数器,它是用来描述资源数目的。P(申请--)
、V释放++
b.信号量集函数
-
semget函数
-
semctl函数
-
semop函数
信号量的值与相应资源的使用情况有关,当它的值大于 0 时,表示当前可用的资源数的数量;当它的值小于 0 时,其绝对值表示等待使用该资源的进程个数。信号量的值仅能由 PV 操作来改变。
sembuf结构体
c.二元信号量实现互斥
代码位于
:https://github.com/hansionz/Linux_Code/tree/master/test_sem
对于上述代码的测试结果为A或者B一定是成对出现的,如果将PV操作取消,结果就可能是交叉出现,不会保证其原子性。