进程之间可能会存在特定的协同工作的场景。一个进程要把自己的数据交付给另一个进程,让其进行处理,就叫做进程间的通信。
命令行指令也叫作也个进程,通过|管道实现进程间信息的交互。
进程是具有独立性的,交互数据成本一定很高,所以OS要设计通信方式。
一个进程是看不到另一个进程的资源的。通信的前提是必须得先看到一份公共的资源,有这个媒介才行。
这里的资源就是一段内存!可能以文件方式提供,也可能队列,原始内存块也可能。这也就是通信方式有很多中的原因。
这个公共资源应该属于谁?OS。进程具有独立性所以不属于其中任何一个进程。
进程间通信的本质,就是OS参与,提供一份有通信进程能看到的公共资源。
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就不再写入了,因为管道有大小。
因为要让reader来读啊。不写了的本质是要等对方来读。
原子性写入,4kb大小的管道空间读完了,才会继续写入。
操作验证写入:
子进程sleep,父进程一直读取实现,写的很慢,读的很快,读端等写端。
如果写端关闭了,也就是子进程写入一条消息之后,直接关闭管道并且退出,父进程一直在读,子进程退出时得到fd返回值为0。
父进程退出,而子进程一直在写,但是读完一条之后,现象是两条进程都没了。
因为如果读端退出了,OS角度再写入就没有意义了。这时OS就把子进程杀掉了,给子进程发送SIGPIPE信号。
父进程退出,父进程此时可以读取子进程的退出信息在子进程异常退出时。
退出码和信号来查看退出信息。退出信号就是13,SIGPIPE信号。
读端不读或者读的慢,写端就等读端
读端关闭,写端收到SIGPIPE信号直接中止
写端不写或者写的慢,读端等写端
写端关闭,读端read读完内部数据后返回到0,标明读到文件结尾。
具有血缘关系的进程进行进程间通信,常用于父子通信
管道是一个只能单向通信的通信信道
是面向字节流的
互斥和同步的
管道是文件吗?
如果文件只被当前进程打开,进程退出了(会自动递减struct file中的ref引用计数),文件呢?会被OS自动关闭。所以,
##命名管道 fifo
专用命令:mkfifo
标识一个磁盘文件用什么方案呢?
路径/文件名 (具有唯一性吗?肯定的,树状结构,向上追溯唯一路径)。
一个进程写入文件后,关闭文件,另一个进程可以读取。
将磁盘中的文件加载到内存,操作文件时而不要把数据刷新到磁盘,将文件加载到内存中,一个进程写一个进程读,加快效率。
那两个进程如何看到同一份资源?那两个进程如何看到同一个文件的呢?路径+文件名确定磁盘中的唯一文件
命名管道:通过文件名确定唯一性,实现看到同一份资源。
文件创建时的权限我们设置是0666,显示时是0644,是受到系统掩码的限制的,为了实现权限就是我们想设计的,将系统掩码设置为0:umask(0);
建议使用系统调用接口,没有缓冲区的影响。
系统调用接口,键盘输入时的\n字符也会输入进系统调用接口。C语言会过滤掉那个\n
执行时先将创建管道的文件执行。
中止时,client退出,server自动退出。命名管道体现到内个有名字的fifo.
因为命名管道也是基于字节流的,所以实际上信息传递时,是需要通信双方定制协议的。(先不考虑)
以上都是基于文件的通信方式下面是SystemV标准的进程间通信方式
在OS层面专门为进程间通信设计的一套方案,
谁设计的?计算机科学家+程序员
要不要给用户用?肯定是
以什么方式??OS不相信任何人,提供接口给用户。
System V进程间通信,一定会存在专门用来通信的接口(system call)。
就需要有人和组织机构等来定制标准。在同一主机内的进程间通信方案就是SystemV方案。
通过某种调用在内存中创建一份内存空间
通过某种调用让进程“挂接”到这份新开辟的内存空间上!
去关联,去挂接,清理内存。
进程就是参与通信的进程。
实现了让不同的继承看到同一份资源。
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值。
shmget()
,故意设置SIZE是4097不是4KB的整数倍。 系统的IPC资源声明周期是随内核的,申请创建出来属于OS,不属于进程。只能通过程序员显示的释放,或者是操作系统重启。所以即使继承运行结束了,曾经创建的共享内存也没有被释放。
key 只是用来在OS层面标识唯一性的不能用来管理共享内存。shmid是OS给用户返回的id,用来在用户层进行shm管理。
命令行是在用户层还是内核层?用户层,所以用shmid。
连续申请shmid是递增的,数组下标的形式组织的。监控脚本:while :; do ipcs -m sleep 1;echo '########'; done
,10s后释放掉共享内存。
perms一直是0。在创建共享内存的时候加上0666。创建共享内存的权限就有了,也是依赖于文件系统的,一切皆文件的细节体现。
void shmat(shmid,NULL,0);
默认情况下,挂接的地址由OS决定,所以设置为NULL,shmflg=0;
返回的地址是虚拟地址肯定不是物理地址,只要是返回给用户就全都是虚拟地址。
malloc返回的是堆空间地址返回的也是虚拟地址。
shmdt(shmaddr);
成功返回0。去冠梁不是释放共享内存而是取消当前进程和共享内存的关系。
演示整个声明周期: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只需要获取即可。单独使用IPC_CREAT有就拿没有就创建
client 根本不需要删除挂接就行。然后去关联就行。
两个进程和共享内存发生挂起和去关联。
实验:client端只是挂接即可并不做任何业务逻辑。(后续由于detach以及共享内存的释放,nattach无值)
这里有没有像管道一样的调用read这样的接口呢?
没有,所以,共享内存一旦建立好并映射进入自己的进程地址空间,该进程就可以直接看到共享内存,就如同malloc 的空间一样,不用任何接口。
read和write的本质就是将数据从内核拷贝到用户,或者从用户拷贝到内核。从一个用户的数据刷到内核缓冲区,另一个再去拿,都需要系统调用接口。
当client没有写入的时候,server仍然在读取共享内存,并没有进行等待写入端(上图中的server刷空格)。
是所有的进程间通信速度最快的。
共享内存不提供任何同步和互斥机制,需要程序员自行保证数据的安全。比如一句话读一半就走了,让字符串的含义发生了变化
共享内存的大小建议是4KB的整数倍4096,声明的时候是4097
共享内存在内核中申请的基本单位是页,内存页。
如果我申请4097个字节,内核会给你4096字节*2,就是8KB。
如果你要4097,给你4097个字节显示的,实际上开的数组大小是8KB,但是只有4097个字节元素。
这三个的接口都类似,并且数据结构的第一个结构类型是完全一样的。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本身也是临界资源,信号量本身也是临界资源。
互斥
二元信号量,在任意一个时刻只能允许一执行流进入临界资源执行自己的临界区。排队,一个个通过买东西。