进程间通信之信号量、消息队列、共享内存(system v的shm和mmap)+信号signal

  进程间通信方式有:System v unix提供3种进程间通信IPC:信号量、消息队列、共享内存。此外,传统方法:信号、管道socket套接字

  【注意上述6种方式只能用户层进程间通信。内核内部有类似socket的网络API通信;内核内部或内核与用户间有netlink套接字通信,只支持数据报,但提供全双工;系统调用和sysctl由用户空间发起的通信机制;copy_to_user/copy_from_user可以将数据在用户空间和内核空间相互拷贝;设备可实现ioctl用于两者通信,注册到fasync_queue中后,驱动可kill方式主动通知用户态程序;proc/sysfs等文件系统用于内核空间向用户控件输出信息;内核注册驱动,设置驱动的mmap处理函数,并kmalloc/vmalloc/__get_free_pages分配内存,将这片内存的虚拟地址转换成物理地址(virt_to_phys),再转为页帧,最后remap_pfn_range实现内核物理内存空间映射到用户虚拟地址空间转换的功能,在用户态打开设备,调用mmap即会调用驱动的mmap处理函数,最后返回分配的物理内存的虚拟地址,即内核和用户共享内存,需要信号量同步】

  3个IPC对象共性:IPC对象由魔数和当前命名空间的内核内部ID访问;对内存访问受到权限系统限制(所有者、组、其他);用系统调用分配与IPC对象关联的内存,具有访问权限的进程均可访问;内核中数据结构间关系task_struct->nsproxy->ipc_namespace->ipc_ids->ID到指针的映射->sem_array/msg_queue/shmid_kernel首部都存kern_ipc_perm(标志IPC对象,各内部ID都会关联到一个实例)->各自的队列或者地址空间。

1、信号量semaphore

  信号量是一个特殊的变量,程序对其访问都是原子操作。用于协调多个进程访问临界资源,同步进程。

  系统调用:semget分配信号量集合、semctl初始化信号量集合的值、semop执行对信号量的操作(第二个参数指向数组的指针,数组元素是sembuf,每个元素是对一个信号的操作。可实现对临界区代码的操作)、semtimedop超时时间设置。

2、消息队列

   发送者写数据到消息队列,一个或多个其他进程作为接收者去队列获取信息,消息=正数表示类型+消息正文,消息读取即删除,仅一个进程可读取到,读写无须同时,内核会保存。

  消息封装在msg_msg实例,每个消息至少分配一个内存页,msg_msg处于该页首部,成员变量msg_msgseg *next指向下一页,因此消息可分布到任意数目的页上

3、共享内存

3.1 各个进程如何寻址到同一共享内存区域的内存页面

  涉及linux存储管理和文件系统:  

  1、address_space+偏移量->page物理页面:被访问文件物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

  2、文件->inode->address_space:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

  3、新建共享内存并不真正改变进程的页表:当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

  4、对于IPC共享内存映射情况,缺页异常处理程序首先在swap cache(一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上)中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。 注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

3.2 system v的shm共享内存

   通过映射特殊文件系统shm中的文件实现进程间共享内存通信,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的)。通信进程需将同一段共享内存连接到各自的地址空间,需通过信号量进行同步。

  系统调用:Shmget创建共享内存得到与魔数相关的标识符,初始化该共享内存区相应的shmid_kernel结构时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。即创建了文件系统shm中的一个同名文件与共享内存区域相对应。主要结构如下:

  shmat将共享内存连接到当前进程地址空间,启动对其的访问。只是映射文件系统shm中的同名文件过程,原理与mmap大同小异;shmdt将共享内存从当前进程分离;shmctl控制共享内存,设置command参数SPC_RMID可删除共享内存段。

3.3 mmap实现共享内存(也可用于进程本身快速访问文件内容)

  mmap()系统调用使得进程(之间)通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read,write等操作。

       系统调用:void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset ) :参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_ANONYMOUS,MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址。

  munmap在进程地址空间中解除一个映射关系解除后,对原来映射地址的访问将导致段错误发生;msync进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap后才执行该操作。可以通过调用msync实现磁盘上文件内容与共享内存区的内容一致。

  mmap用于共享内存的两种方式:(1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap:ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0)。 (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap,然后调用fork。那么在调用fork之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap返回的地址,却由父子进程共同维护。对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0)。

         对mmap返回地址的访问:对于用mmap映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。

3.4 两种共享内存方式对比

  优势:效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。

  使用:共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,则可将映射到的地址类型转换,而后按结构体指针访问。

  system v共享内存中的数据,从来不写入到实际磁盘文件中去,因其是映射到特殊文件系统shm的文件;而通过mmap映射普通文件实现的共享内存通信可以msync指定何时将数据写入磁盘文件中。

  system v共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

 4、信号signal、sigaction

  Kill –l可以看各信号。进程可阻塞/屏蔽一个信号,直到解除屏蔽,在阻塞期间到达的信号会放在待决列表,同一信号阻塞多次只放一次(SIGKILL无法阻塞,无法修改,会立即强行终止进程,但init会忽略该信号)。

  signal和sigaction区别:signal处理中忽略重复信号,但要响应其余信号,sigaction可设置屏蔽位,在处理中阻塞屏蔽的信号,处理后可处理到达的被屏蔽信号:

  1、signal在调用handler之前先把信号的handler指针恢复;sigaction调用之后不会恢复handler指针,直到再次调用sigaction修改handler指针,导致signal丢失信号,而且不能处理/忽略重复的信号,而sigaction就可以处理重复信号。因为signal在得到信号和调用handler之间有个时间把handler恢复了,这样再次接收到此信号就会执行默认的handler(虽然有些调用,在handler的以开头再次置handler,这样只能保证丢信号的概率降低,但是不能保证所有的信号都能正确处理)

  2、signal在调用过程不支持信号屏蔽;sigaction调用后,在handler调用前会把屏蔽信号加入到信号中,handler调用后会自动恢复信号到原先的值。signal处理过程中就不能提供阻塞某些信号的功能,sigaction就可以阻塞指定的信号和本身处理的信号(被阻塞信号会被放在待决列表,同一信号阻塞多次时只放一次(SIGKILL无法阻塞,无法修改,会立即强行终止进程,但init会忽略该信号)),直到handler处理结束,就可以再次接受重复的信号。

  系统调用:Sigaction实现信号处理程序,sigprocmask通过参数位掩码阻塞为1的信号,sigsuspend进程睡眠,直到有特定信号唤醒进程,避免忙等待,sigpending检查是否有待决信号,sigreturn恢复进程上下文,kill向进程组发信号,tkill向单个进程发信号。

  内核流程:kill/tkill会走到do_tkill->find_task_vpid找到发送信号进程->check_kill_permission检查是否有权限发送信号->specific_send_sig_info信号处理工作->sig_ignored信号阻塞则放弃->send_signal填充信号数据到目标进程的sigpending链表->signal_wake_up若信号成功发送,唤醒进程使调度器可以选择该进程,设置TIF_SIGPENDING让内核必须将信号传送到进程。但此时还不会触发信号处理程序。而是在每次核心态切换到用户态时,内核会发起信号队列处理do_signal->handle_signal操作进程在用户态的栈,使得从核心态切换到用户态后运行信号处理程序(在用户态运行,防止恶意或不规范代码破坏内核系统安全),而不是正常代码->sigreturn恢复进程上下文,使下一次切换到用户态下,进程可以正常运行。

参考:《深入Linux内核架构》,其余已在文中有链接。

原文地址:https://www.cnblogs.com/beixiaobei/p/10497966.html