文章目录
进程间的通信
-
进程之间可能会存在特定的协同工作的场景。一个进程要把自己的数据交付给另一个进程,让其进行处理,就叫做进程间的通信。
命令行指令也叫作也个进程,通过|管道实现进程间信息的交互。
-
进程是具有独立性的,交互数据成本一定很高,所以OS要设计通信方式。
一个进程是看不到另一个进程的资源的。通信的前提是必须得先看到一份公共的资源,有这个媒介才行。这里的资源就是一段内存!可能以文件方式提供,也可能队列,原始内存块也可能。这也就是通信方式有很多中的原因。
-
这个公共资源应该属于谁?OS。进程具有独立性所以不属于其中任何一个进程。
-
进程间通信的本质,就是OS参与,提供一份有通信进程能看到的公共资源。
pipe的本质:是通过子进程继承父进程的资源的特性,达到一个让不同的进程看到同一份资源!
-
管道
匿名管道pipe
匿名管道就是基于父子进程的。
管道通信原理
那么创建子进程时,给子进程一份PCB进程控制块,struct files* struct{}
也得给子进程拷贝一份。
因为struct files_struct {}
文件描述符表属于进程部分,也就是属于父进程,所以子进程也得有一份,
而包含文件属性的struct file{}
属于文件操作,不需要给子进程拷贝一份,因为进程和文件只具有关联关系。
父子进程通过不同的文件描述符数组看到一个文件结构体,从而实现对于同一个文件的操作。
ps
:文件结构体struct file{}里面有文件属性和文件操作的指针集合(operator()),struct operator{int *write···等各种操作指针}
,根据不同的外设文件调用具体不同的方法,实现了一切皆文件操作。
父进程操作时,
上层调用系统调用接口write();将字符串直接刷新到OS为文件创建的文件内核缓冲区。
write的正常的操作是拷贝数据从用户到内核,触发底层struct oper{}
写入函数,从内核写入到磁盘。
这样,父子进程此时看到的是同一个struct file{}也就是相同的资源,
而当父进程进行写入到内核缓冲区但是不刷新到磁盘上时,
此时子进程可以依靠同样的文件操作表中的指针找到相同的struct file{}
,到文件内核缓冲区中,找到并读取同样位置的由父进程处理过的文件数据,
这种基于文件之间的通信方式就是管道,既不属于进程也不属于文件操作部分,由OS关联。
就是一个向文件缓冲区写数据,一个读数据就实现了两个进程之间的通信。
原理如下图所示:
代码层面原理:
管道是一个单向的通信信道
操作:父子进程看到同一份资源,单向管道。一个写入一个读取(用close()关闭文件描述符)
专门的函数pipe()
,pipefd[2]
是一个输出型参数,我们想通过这个参数读取到打开的两个fd。
读取写入时的系统调用函数ssize_t s = read();
如果返回值fd=0,意味着子进程关闭文件描述符了就不写了。s>0,可以继续读,s<0说明读取出现问题。
写入的时候不需要将\0写入到管道文件当中,父进程读的时候手动添加\0,意思就是我把他当成字符串来看待。
总结:
管道的本质就是一种文件,实际上是一种内核缓冲区,大小是4KB。
管道创建以后会产生两个文件描述符,一个是读端一个是写端
管道里的数据只能在写端被写入从读端被读出。
进程对于管道进行读写操作都可能被阻塞。
管道为空读操作会被阻塞;管道满了,写操作会被阻塞。
管道可以有多个进程对其读,但是不能是同时的。
匿名管道是单向的,命名管道可以实现双向。
管道是在内存中的,容量不受磁盘的限制。
探索管道特性:
管道是面向字节流的
让父进程sleep,子进程不sleep,这样是读的慢写的快,
造成pipe里面只要有缓冲区就一直写,read只要有数据就一直读取。
出现问题:出现的读取时不连续的,不符合语句逻辑的。
我们不能理想化的认为按语句读取,按字节读,字节流。只有字节的概念。打印上一批数据。
tcp FILE fstream
- 现在的管道仅限于父子通信
子进程写入,但是父进程不读。
写满64KB时,write就不再写入了,因为管道有大小。
- 当write写满的时候,为什么就不写了呢?可以覆盖的啊!
因为要让reader来读啊。不写了的本质是要等对方来读。
- 如果父进程每隔十秒读一个字符,
前十秒子进程刷屏,10秒后,读一个。没反应,写也没有继续写入,所以,
必须得多读一些才能够让子进程继续写入一点。所以,
管道自带同步机制
原子性写入,4kb大小的管道空间读完了,才会继续写入。
操作验证写入:
-
子进程sleep,父进程一直读取实现,写的很慢,读的很快,读端等写端。
-
如果写端关闭了,也就是子进程写入一条消息之后,直接关闭管道并且退出,父进程一直在读,子进程退出时得到fd返回值为0。
- 写端一直在写,不退出。父进程读端读一条消息就break,关闭读口。
父进程退出,而子进程一直在写,但是读完一条之后,现象是两条进程都没了。
因为如果读端退出了,OS角度再写入就没有意义了。这时OS就把子进程杀掉了,给子进程发送SIGPIPE信号。
父进程退出,父进程此时可以读取子进程的退出信息在子进程异常退出时。
退出码和信号来查看退出信息。退出信号就是13,SIGPIPE信号。
总结
四种情况:
读端不读或者读的慢,写端就等读端
读端关闭,写端收到SIGPIPE信号直接中止
写端不写或者写的慢,读端等写端
写端关闭,读端read读完内部数据后返回到0,标明读到文件结尾。
五个特点:
-
具有血缘关系的进程进行进程间通信,常用于父子通信
-
管道是一个只能单向通信的通信信道
-
是面向字节流的
-
互斥和同步的
管道是文件吗?
如果文件只被当前进程打开,进程退出了(会自动递减struct file中的ref引用计数),文件呢?会被OS自动关闭。所以,
- 管道的生命周期是随进程的。
##命名管道 fifo
专用命令:mkfifo
- 命令行命名管道的创建和使用
命名管道理解
-
标识一个磁盘文件用什么方案呢?
路径/文件名 (具有唯一性吗?肯定的,树状结构,向上追溯唯一路径)。
一个进程写入文件后,关闭文件,另一个进程可以读取。
将磁盘中的文件加载到内存,操作文件时而不要把数据刷新到磁盘,将文件加载到内存中,一个进程写一个进程读,加快效率。
那两个进程如何看到同一份资源?那两个进程如何看到同一个文件的呢?路径+文件名确定磁盘中的唯一文件
命名管道:通过文件名确定唯一性,实现看到同一份资源。
编码操作mkfifo
文件创建时的权限我们设置是0666,显示时是0644,是受到系统掩码的限制的,为了实现权限就是我们想设计的,将系统掩码设置为0:umask(0);
- 一旦我们具有了一个命名管道此时我们只需要让双方进行文件操作就行。
建议使用系统调用接口,没有缓冲区的影响。
系统调用接口,键盘输入时的\n字符也会输入进系统调用接口。C语言会过滤掉那个\n
执行时先将创建管道的文件执行。
中止时,client退出,server自动退出。命名管道体现到内个有名字的fifo.
因为命名管道也是基于字节流的,所以实际上信息传递时,是需要通信双方定制协议的。(先不考虑)
通信实现进程间控制
- sever端对client端输入的字符串进行命令解析:
-
让server端sleeo几秒之后再进行读取
-
为什么命名管道要有名字呢?为了保证不同进程看到同一个文件,必须有名字。
匿名管道通过父子继承的方式继承同一个文件描述符表,不需要名字标识唯一性。
以上都是基于文件的通信方式下面是SystemV标准的进程间通信方式
SystemV
在OS层面专门为进程间通信设计的一套方案,
谁设计的?计算机科学家+程序员
要不要给用户用?肯定是
以什么方式??OS不相信任何人,提供接口给用户。
System V进程间通信,一定会存在专门用来通信的接口(system call)。
就需要有人和组织机构等来定制标准。在同一主机内的进程间通信方案就是SystemV方案。
- 进程间通信的本质,就是OS参与,提供一份有通信进程能看到的公共资源。
system V主流方案有:
1.共享内存
2.消息队列
3.信号量:实现进程间同步和互斥
共享内存
原理:
-
通过某种调用在内存中创建一份内存空间
-
通过某种调用让进程“挂接”到这份新开辟的内存空间上!
-
去关联,去挂接,清理内存。
进程就是参与通信的进程。
实现了让不同的继承看到同一份资源。 -
OS内可不可能存在多个进程,同时使用不同的共享内存来进行进程间的通信?
共享内存在系统中可能有多份,OS要管理这些不同的共享内存。如何管理ne?先描述在组织。
一定创建了内核数据结构,描述这些共享内存的属性,对这些共享内存的管理变为对链表的增删查改。
-
你怎么保证,两个或者多个看到的是一个共享内存呢?
共享内存一定有标识唯一性的id,方便让不同的进程访问同一个资源这个id在哪里呢?在描述共享内存的结构体里面。就像进程内个pid在PCB中。
接口:
-
创建共享内存:
-
控制共享内存:
- 关联和去关联:
attach detach
(1)shmflg:
IPC_CREAT
单独使用,或者flg是 0,创建一个共享内存,如果创建的内存已经存在,就直接返回当前
已经存在的共享内存。如果不存在就创建一个。
IPC_EXCL
:单独使用是没有意义的。
IPC_CREAT|IPC_EXCL
:如果不存在共享内存就创建。如果已经有了共享内存就返回出错。如果调用成功,得到的一定是一个最新的没有被别人使用的共享内存。
(2)key_t key;
是一个唯一的标识符,用来进行进程之间额通信。本质是让不同的进程看到同一份资源,你得先给不同的进程看到同一个ID。
ftok();
来帮你确定唯一值。来创建key值。
-
你怎么确定不同进程看到的是一个共享内存?
只要形成key的算法和原始数据是一样的就同一个ID
这里的key就是会被设置进入描述共享内存shared_memory
的结构体中。
两个进程应该用相同的ftok()方法,实现得到的是一样的key,描述的是一样的共享内存。
类似命名管道,需要相同的路径名确定管道一样,需要相同的算法函数创建得到一样的keyid值。
- 得到key之后就要创建共享内存了
shmget()
,故意设置SIZE是4097不是4KB的整数倍。
- ipc -m :查看共享内存。
系统的IPC资源声明周期是随内核的,申请创建出来属于OS,不属于进程。只能通过程序员显示的释放,或者是操作系统重启。所以即使继承运行结束了,曾经创建的共享内存也没有被释放。
- 删除用ipcrm -m shmid
key 只是用来在OS层面标识唯一性的不能用来管理共享内存。shmid是OS给用户返回的id,用来在用户层进行shm管理。
命令行是在用户层还是内核层?用户层,所以用shmid。
-
程序员删除用接口
shmctl(shmid,cmd,struct shmid_ds* buf);
描述共享内存的结构体,属于用户层。
-
连续申请shmid是递增的,数组下标的形式组织的。监控脚本:
while :; do ipcs -m sleep 1;echo '########'; done
,10s后释放掉共享内存。 -
perms一直是0。在创建共享内存的时候加上0666。创建共享内存的权限就有了,也是依赖于文件系统的,一切皆文件的细节体现。
使用共享内存(实验验证SystemV的性质)
void shmat(shmid,NULL,0);
默认情况下,挂接的地址由OS决定,所以设置为NULL,shmflg=0;
返回的地址是虚拟地址肯定不是物理地址,只要是返回给用户就全都是虚拟地址。
malloc返回的是堆空间地址返回的也是虚拟地址。
- 去关联
shmdt(shmaddr);
成功返回0。去冠梁不是释放共享内存而是取消当前进程和共享内存的关系。
server端
演示整个声明周期:shmid=15
int main()
{
key_t key =ftok(PATH_NAME,PRO_ID);
if(key< 0)
{
perror("ftok error");
return 1;
}
int shmid = shmget(key, SIZE , IPC_EXCL|IPC_CREAT|0666);
printf("key:%u shmid: %d \n",key,shmid);
sleep(10);
char* mem=(char*)shmat(shmid,NULL,0);
printf("attaches shm success\n");
sleep(5);
//化解完成之后进行通信逻辑
shmdt(mem);
printf("detach shm success\n");
shmctl(shmid,IPC_RMID,NULL);
printf("key: 0x%d, shmid :%d -> shm delete success\n",key,shmid);
sleep(5);
return 0;
}
client端
client只需要获取即可。单独使用IPC_CREAT有就拿没有就创建
client 根本不需要删除挂接就行。然后去关联就行。
两个进程和共享内存发生挂起和去关联。
实验:client端只是挂接即可并不做任何业务逻辑。(后续由于detach以及共享内存的释放,nattach无值)
实验:client进行写入,server一直读取
-
这里有没有像管道一样的调用read这样的接口呢?
没有,所以,共享内存一旦建立好并映射进入自己的进程地址空间,该进程就可以直接看到共享内存,就如同malloc 的空间一样,不用任何接口。read和write的本质就是将数据从内核拷贝到用户,或者从用户拷贝到内核。从一个用户的数据刷到内核缓冲区,另一个再去拿,都需要系统调用接口。
当client没有写入的时候,server仍然在读取共享内存,并没有进行等待写入端(上图中的server刷空格)。
-
是所有的进程间通信速度最快的。
-
共享内存不提供任何同步和互斥机制,需要程序员自行保证数据的安全。比如一句话读一半就走了,让字符串的含义发生了变化
补充
共享内存的大小建议是4KB的整数倍4096,声明的时候是4097
共享内存在内核中申请的基本单位是页,内存页。
如果我申请4097个字节,内核会给你4096字节*2,就是8KB。
如果你要4097,给你4097个字节显示的,实际上开的数组大小是8KB,但是只有4097个字节元素。
- 只要key值相等,就能找到一个共享内存:第一个进程过来找到shmid_ds{}->ipc_perm->key,第二个进程来同样如此,所以只要是key相等就行。
- msgget();消息队列,同样有key,那么就代表只要key相同,就可以找到同一个消息队列。
- semget();信号量
这三个的接口都类似,并且数据结构的第一个结构类型是完全一样的。struct ipc_perm{};
所有的ipc资源都是通过数组组织起来的(就内个shmid是主键递增的)。
所有的systemV标准的ipc资源(互不一样),但是XXXid_ds
结构的第一个成员结构体都是ipc_perm(一样的)。
所有的ipc资源的头部都有struct ipc_perm{}
结构体类型,那么我们声明一个struct ipc_perm *
的一个数组类型,数组下标依次的指向各个ipc资源,实现对于各种ipc资源的整理。
如果要访问某一资源,数组下标就行。
如果我们要访问某一资源的其他类型属性时,只需要进行具体类型强转就可以实现访问。
C语言的形式实现C++的切片的效果
-
为何我们看到的ipc资源的shmid是不断增长的呢?
他就是内个数组下标
信号量初认识
申请是semget();删除信号量semctl();
管道共享内存消息队列都是传输数据为目的的!
信号量是通过共性资源的方式,来实现多个进程的同步和互斥的效果。
信号量的本质是一个计数器,类似int count;
衡量临界资源中资源数目的。
-
什么是临界资源
凡是被多个执行流同时能够访问的资源就是临界资源。比如多进程同时向显示器打印。
进程间通信的本质就是让不同的进程看到同一份资源,管道共性内存消息对列都是临界资源。
凡是要进程间通信,必定引入多个进程看得到的同一份资源,同时他就变成了临界资源。 -
什么是临界区
进程的代码是很多的,其中用来访问临界资源的代码叫做临界区。比如都往显示器打印的printf();电影院的某一个放映厅,是一个临界资源!是不是我坐在放映厅的座位上,这个座位才属于我?不是
票买了,人没去。,买到票的时候就属于我了。
买票的本质就是对临界资源的预定机制。
一个放映厅票卖多了,最多只能卖100张票,信号量count来约束。 -
什么是原子性?
一件事情要么不做,要么就做完。没有中间态,就是二极管思维。有中间过程就是非原子性。多进程访问内存资源,假设count=100,父进程需要对他计算,所以要将count加载到CPU中运算之后返回内存重新写入为99.如果在CPU刚计算完值之后还没来得及写入就被切走,那么在父进程的上下文数据中count=99并且离开了。此时子进程进来同样执行计算操作,子进程完成计算写入到内存count=5离开。此时父进程回来了继续未完成的写入工作误将count又设置为99,造成对于全局变量的误操作。
count--
本身不是原子。所以买票本身不是原子。
每个人都想进电影院,必须先有信号量count–;退出就++;前提是每个人都看到count;
count本身也是临界资源,信号量本身也是临界资源。- 自己怎么保证自己的安全呢?
只要内部对于+±-操作是原子的。(–)p()操作 (++)V()操作保障了信号量的稳定和安全。
- 自己怎么保证自己的安全呢?
-
互斥
二元信号量,在任意一个时刻只能允许一执行流进入临界资源执行自己的临界区。排队,一个个通过买东西。