为什么要进行进程间通信?
因为进程间具有独立性(每一个进程都有自己的虚拟地址空间,进程A并不知道进程B的虚拟地址空间中的内容),因此导致了进程之间协作的问题
进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程
数据共享:多个进程间需要共享同样的数据、资源
进程控制:一个进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信分类:
管道
System V IPC
POSIX IPC
网络是当前最大的进程间通信
本篇文章中只针对管道及System V中的共享内存、消息队列和信号量进行解释
概念:
我们把从一个进程连接到另一个进程的一个数据流称为一个管道
管道是Unix中最古老的进程间通信的形式。它本质是一个内核中的内存,也可以将这块内存称为缓冲区,当其中的数据被读走后,管道就为空
管道是半双工的,即数据只能由一个流向
# include
int pipe(int fd[2]);
功能:创建一个匿名管道
参数:fd[]:文件描述符数组,fd[0]表示读端,fd[1]表示写端,使用这一对文件描述符访问内存
返回值:成功返回0,失败返回-1
【注】:
1、pipe()函数的参数是输出型参数,也就是意味着需要传入一个int类型数组,数组大小为2,每个元素保存的都是文件描述符,当函数成功执行后,会在内核中开辟一块内存,即所谓的管道,将管道的读端和写端分别赋给fd[0]和fd[1]
2、管道使用完后,需要及时关闭文件描述符
3、管道的大小:
PIPE_SIZE为64k
PIPE_BUF为4k,指的是单次写入管道的数据大小
若是写入的数据大于PIPE_BUF,则不能保证数据的原子性
1、匿名管道只能用于具有亲缘关系的进程间通信,因为访问匿名管道前提是必须知道管道的读写端的文件描述符信息,可是,没有亲缘关系的进程间互相不知道对方创建的文件描述符信息,所以不能共同操纵同一个管道进行通信。但是,在创建进程时,子进程会继承父进程的文件描述符表,因此能够获取到父进程创建的管道的文件描述符信息,所以能够访问同一管道进行通信。此外,匿名管道没有标识,所以不能被任意进程访问
2、半双工,数据单向传输
3、提供字节流服务
4、管道的生命周期随进程
5、
如果管道为空,则读阻塞
如果管道为满,则写阻塞
如果管道的读端被关闭,则写端往管道中写数据的时候会造成管道破裂,并且导致进程收到SIGPIPE信号,从而终止进程
如果管道的写端被关闭,则读端读完管道中的数据之后,读端继续读的话不会陷入阻塞状态,而是返回,执行代码的正常流程
6、管道自带同步互斥功能。在规定的一次写入数据大小范围内,数据具有原子性,即当前的读写不会被打断。同步:保证临界资源访问的合理性;互斥:同一时间,保证只能由一个进程访问临界资源。临界资源:同一时间,当前资源只能被一个进程所访问
概念;
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
如果想在任意进程间都能进行通信,可以使用FIFO文件来解决,它被称为命名管道
命名管道是一种特殊类型的文件
命名管道是具有标识符的管道,本质是内核中的一片内存,也称之为“缓冲区”
可以通过访问命名管道文件来访问内核中的缓冲区
1、命令行创建
mkfifo [filename]
2、通过函数创建
int mkfifo(const char *filename, size_t mode);
int main()
{
mkfifo("test", 0664);
return 0;
}
1、具有标识符,可以满足不同进程进行进程间通信
2、声明周期跟随进程
3、其他特性都跟随匿名管道
FIFO与pipe之间唯一区别在于他们创建和打开的方式不同,一旦这些工作完成之后,他们具有相同的语义。
匿名管道有pipe函数创建并打开
命名管道有mkfifo函数创建,用open打开
创建共享内存时,首先在物理内存中创建一块内存,各个进程都通过页表结构将该段内存映射到自己的虚拟地址空间上的共享区,各个进程通过映射的地址来进行通信
1、共享内存是最快的进程间通信。共享内存直接访问内存就可完成进程间的通信,而管道涉及到用户态和内核态直接的数据的相互拷贝,效率较低
2、共享内存不带有同步互斥功能
3、对共享内存写入数据是按照覆盖的方式进行的
4、共享内存的生命周期随操作系统内核
int shmget(key_t key, size_t size, int shmflg);
参数:
key:共享内存段标识符
size:共享内存大小
shmflg:
IPC_CREAT:如果共享内存不存在,则创建共享内存;如果已经存在,则返回共享内存
IPC_CREAT | IPC_EXCL:如果共享内存已经存在,则报错返回;如果不存在则创建共享内存
加上权限:按位或上权限,即给共享内存加上权限,权限为8进制数字,类似文件的权限
返回值:
成功返回共享内存的操作句柄,失败返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存的操作句柄
shmaddr:表示映射到共享区的哪个地址,一般情况下选择NULL,由操作系统自己指定将内存映射到虚拟地址空间的哪个位置
shmflg:
0:可读可写
IPC_RDONLY:只读
返回值:
返回映射到的地址
int shmdt(const void *shmaddr);
参数:
shmaddr:shmat()的返回值
返回值:
成功返回0,失败返回-1
int shmct(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存的操作句柄
cmd:要执行的操作
IPC_STAT:获取当前共享内存的状态,要搭配shmid_ds一起使用
IPC_RMID:删除共享内存,标记共享内存为删除状态
IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
buf:输出型参数,结构体中是一些共享内存信息
返回值:
失败返回-1
//readShm.c
#include
#include
#include
#define shm_key 0x12345678
int main()
{
//创建共享内存
//shmget
int shmid = shmget(shm_key, 1024, IPC_CREAT | 0664);
if(shmid < 0)
{
return 0;
}
printf("%d\n", shmid);
struct shmid_ds buf;
shmctl(shmid, IPC_STAT, &buf);
printf("shm_size : %ld\n", buf.shm_segsz);
void *lp = shmat(shmid, NULL, 0);
if(!lp)
{
return 0;
}
while(1)
{
printf("%s\n", (char*)lp);
sleep(1);
}
shmdt(lp);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
//writerShm.c
#include
#include
#include
#define shm_key 0x12345678
int main()
{
//创建共享内存
//shmget
int shmid = shmget(shm_key, 1024, IPC_CREAT | 0664);
if(shmid < 0)
{
return 0;
}
printf("%d\n", shmid);
struct shmid_ds buf;
shmctl(shmid, IPC_STAT, &buf);
printf("shm_size : %ld\n", buf.shm_segsz);
void *lp = shmat(shmid, NULL, 0);
if(!lp)
{
return 0;
}
while(1)
{
sprintf((char*)lp, "%s", "hello\n");
sleep(1);
}
shmdt(lp);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
ipcs -m #查看共享内存
ipcrm -m [shmid] #删除一个共享内存
【注】:如果删除了一个有进程附加的共享内存,操作系统的做法是,先标记当前的共享内存为destory状态,并将key设置为0x00000000,表示当前的共享内存不能再被其它进程所附加,同时会释放内存,也就导致了正在附加该共享内存上的进程有崩溃的风险,一般禁止这样去做。当附加的进程退出的时候,操作系统就会将该共享内存清理掉
消息队列本质上是内核中的一个优先级队列,进程通过访问优先级队列来增加节点或查看节点,来进行进程间通信
1、生命周期随进程,如果用户进程不删除消息队列资源,则该资源一直在操作系统内核中
2、消息队列自带同步互斥机制
信号量本质上是一个计数器 加 PCB等待队列,是对资源的计数
实现进程的控制,即实现进程的同步与互斥
进程互斥 :由于各进程要求资源共享,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源
临界资源 :系统中某些资源一次只允许一个进程使用
临界区 :进程中涉及到互斥资源的程序段
实现互斥:
1、信号量只有两个取值----0 / 1,0表示当前资源不可用,1表示当前资源可用
2、当进程需要访问一个临界资源的时候,会先访问信号量,预计算信号量的值:
对当前的信号量进行预-1操作,判断当前信号是否小于0
如果小于0,则表示信号量之前的值是0,即当前的临界资源不可用,那么就将当前的进程放到PCB等待队列中去
如果等于0,则表示信号量之前的值是1,即当前的临界资源可用,访问临界资源,并且将信号量-1,标志着当前进程在访问临界资源期间,其他进程不能访问该临界资源
3、如果已经完成了访问,则需要结束对临界资源的访问,将信号量+1
-1操作,我们称之为P操作,+1操作,我们称之为V操作。这就是所谓的PV操作(逸闻:PV分别对应荷兰语中的减加,并不是英语单词首字母)
同步保证了合理性
1、在实现同步时,信号量的取值为大于0和小于0,信号量大于0表示有多少个资源可以使用,信号量小于0时,信号量的绝对值表示有多少个进程在等待资源
2、如果需要访问一个资源时,将信号量-1,访问资源
3、如果访问资源完成时,将信号量+1
4、如果当前的信号量值是小于0的,表示当前PCB等待队列中还有进程在等待资源,当一个进程结束对资源的访问的时候,应当唤醒PCB等待队列中的一个进程,使之去获取资源
5、如果当前的信号量值是大于0的,表示PCB等待队列中没有进程在等待,则可以直接获取资源