进程通信方式总结
1.管道(pipeline)
优点:简单
缺点:1.信息无边界
2.只能单向写,读
3.信息无优先级
4.一般要阻塞,就算使用 O_NONBLOCK 标志位来达到不阻塞,也要一次性把管道写满才能不阻塞,但是无法知道管道可写空间是多少
5.发送信息少的时候,使用管道效率低,因为需要从用户态转为内核态,进行上下文切换,而且要从用户空间拷贝内容到内核空间
6.要占用打开文件号
7.不能用 lseek() 改变读写指针的位置
8.匿名管道只能在父进程创建,只有通过fork或clone等系统调用产生的子进程能获得这个管道的文件打开号读写管道,而且对管道的关闭和打开需要在C++语言层面自己调用close关闭
命名管道的话能够,任何进程都可以通过相同的文件路径访问他(普通管道用pipe(文件打开号)创建,然后传给子进程 | 匿名管道用 open 打开,标志位可以设置只读,只写等属性或者O_NONBLOCK
[是否是读的话没有数据直接退出,如果是写的话要一次写完能写的空间才退出])
命名管道有一个 inode , 用open打开他的时候如果没有分配内存页就为他分配内存页(GFP_USER?)
2.信息(message)
优点:相比管道,信息可以按照报文为单位传输数据
缺点:发送还是需要拷贝用户空间数据到内核空间,接受还是要从内核空间拷贝到用户空间(put_user,store_msg)
最大报文长度有限制,Linux有两个宏去限制最大报文长度
3.共享内存(shared memory)
共享内存也使用ipc通用结构管理,每个进程只要找到特定的标识符就可以将某段共享内存和自己的虚拟内存映射挂钩
实际依赖的是 do_mmap ,标识符是 SHARED 或 SHARED | FIXED , (FIXED是申请到的地址和需要的地址一样的时候设置的)
具体的 no_page 函数是 shmem_nopapge, 这个函数会凭借 inode 持有的缓存结构 address_space 和地址对应的页面号去 缓存杂凑队列里找 page
如果缓存队列里找不到,就要去看看inode的i_data结构,共享内存下是 shmem_inode_info , 里面保有文件一级,二级,三级 映射的数组,通过 页面号可以找到 swap_entry_t , 这个 swap_entry_t 相当于交换设备的块号,可以把页面找到并交换进来
优点:效率高
缺点:需要同步机制
4.软中断 或称信号(signal)
task_struct 有四个最核心的和信号相关的域:
1.signal_struct *sig //这里面重要的是一个 action 数组,存有相当于硬中断中断向量的sigaction(其实是函数指针)
2.sigset_t blocked //对上面的sigset_t类型位图的掩码,sigset_t就是一个long型,信号投过来会把某一位置位1
3.struct sigpending pending //保存有一个 sigset_t 和 一个 sig_queue ,这个sigset_t 就是保存其他进程投递过来的信号的位图,这个sig_queue可以找到发信号过来的进程,貌似只要找到就行了,不用知道是谁发了什么信号
一个进程发送信号给其他进程其实就是向 task_struct 里的 pending 里的 sigset_t 设置位,被投递进程从内核空间退出的时候,就会把这个位抹掉,并且调用相应的软中断函数(信号其实就是软中断),所以信号函数的产生可能有延时
优点:软件实现的中断,可以记录同一中断信号具体有哪些进程发送来过,硬中断就算多个进程发送同一中断信号,最后只能扫描所有发送这种中断信号的信号源
缺点:有延时,需要从内核空间退出才能调用中断函数
5.信号量(semaphore)
也是使用 ipc 通用结构,代表结构是 sem_array ,其中比较重要的三个域:
1.sem_base // 实质上最根本的东西,就是用来记录信号量状态的,是一个 sem 结构数组,每个sem都是由 一个 代表信号量本身位图的 semval 和 最后操作进程的id 组合成的结构体
2.sem_pending // 记录等待者的一条队列,假设一个进程A 想要同时获得 信号量a 的两个单位,信号量b的1个单位,如果不能原子性取得,则会睡眠,同时把自己挂在这条队列上。等到其他进程释放信号量,会尝试
去遍历这个队列,以不更改上面 sem_base 的方式测试一下是否能满足当前遍历到的 进程的要求,如果能满足就唤醒这个进程
这条队列每个节点包含两个重要信息:1.等待者,2.信号量操作集合(要对哪些信号量增减多少)
3.undo
也是一条队列,每当进程成功执行了一个信号量操作集合,就会在这条队列上记账,即把自己的信息和借走了多少信号量封装成一个节点,放进这个队列(同时放入task_struct的队列)。
在释放信号量的时候会把这个节点出对,也就是销账。如果一个进程 exit 退出但是没有销账,那么内核代码会帮忙销账,task_struct 里有一个 semundo 指针,和上面的 undo 一样类型的队列(节点类型一样)
只不过都是自己欠的账,内核代码会帮忙把这个节点欠的账还清。
对信号量的操作集合是原子的,也就是要么全部操作,要么全部不操作,改变某个信号量的同时会记账,如果无法完成集合里所有信号量的操作,就会通过while循环遍历操作集合,回滚所有操作,并且销账
优点:可以原子性执行一批信号量操作,是通过内核的 锁机制,具体还是要依赖硬件指令(cmpchg等)
6.Socket
通用IPC结构:ipc_ids 里有一个 kern_ipc_perm 数组,并且用一个信号量去保障单进程修改,kern_ipc_perm保存了创建者的 用户信息,包括权限,perm是权限的缩写
kern_ipc_perm 被放入 代表结构中,成为代表结构的域(比如报文队列的代表结构就是 msg_queue, 里面用list_head保存一条msg报文链表)
每个 kern_ipc_perm 都有对应的 key,所以可以通过 key 找到对应的结构
ipc_ids : 代表结构 = 1 : N 并且可以通过 key 找到代表结构
socket本地进程通信的话,就只用发送进程将用户空间内容拷贝到内核里的缓冲区,然后两个进程的各自socket通过指针访问同一个内核的缓冲区(访问sk_buffer,发送进程只是将sk_buffer挂入到接收进程的接收队列),接收进程将这个内核缓冲区的内容拷贝的用户缓冲区,所以是两次拷贝。