进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)

时间:2022-09-20 18:53:19

注:本分类下文章大多整理自《深入分析linux内核源代码》一书,另有参考其他一些资料如《linux内核完全剖析》、《linux c 编程一站式学习》等,只是为了更好地理清系统编程和网络编程中的一些概念性问题,并没有深入地阅读分析源码,我也是草草翻过这本书,请有兴趣的朋友自己参考相关资料。此书出版较早,分析的版本为2.4.16,故出现的一些概念可能跟最新版本内核不同。

此书已经开源,阅读地址 http://www.kerneltravel.net

一、管道

在Linux 中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现如下所述。
• 限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux 中,该缓冲区的大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。
• 读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。
注意,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。
(一)、管道的结构
在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file 结构和VFS 的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个
VFS 索引节点又指向一个物理页面而实现的。如图7.1 所示。
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
图7.1 中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
一个普通的管道仅可供具有共同祖先的两个进程之间共享,并且这个祖先必须已经建立了供它们使用的管道。
注意,在管道中的数据始终以和写数据相同的次序来进行读,这表示lseek()系统调用对管道不起作用。
二、信号
(一)、信号在内核中的表示
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间。
那么,什么时候检测和响应信号呢?通常发生在以下两种情况下:
(1)当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前夕;
(2)当前进程在内核中进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间。
当有信号要响应时,处理器执行路线的示意图如图33.2 所示。
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)

当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:

进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)

进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了):
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
在找到了信号处理函数之后,do_signal() 函数首先把内核堆栈中存放返回执行点的eip保存为old_eip,然后将eip替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:
1、之所以把EIP的值设置成信号处理函数的地址,是因为一旦进程返回用户态,就要去执行信号处理程序,所以EIP要指向信号处理程序而不是原来应该执行的地址。
2、之所以要把frame从内核栈拷贝到用户栈,是因为进程从内核态返回用户态会清理这次调用所用到的内核栈(类似函数调用),内核栈又太小,不能单纯的在栈上保存另一个frame(想象一下嵌套信号处理),而我们需要EAX(系统调用返回值)、EIP这些信息以便执行完信号处理函数后能继续执行程序,所以把它们拷贝到用户态栈以保存起来。
这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
信号处理程序执行完毕之后,进程会主动调用sigreturn()系统调用再次回到内核,查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。
 C++ Code 
1
2
 
(By default,  the  signal  handler  is invoked on the normal process stack.  It is possible to arrange that the signal handler
 uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
三、System V 的IPC 机制
为了提供与其他系统的兼容性,Linux 也支持3 种system Ⅴ的进程间通信机制:消息、信号量(semaphores)和共享内存,Linux 对这些机制的实施大同小异。我们把信号量、消息和共享内存统称System
V IPC 的对象,每一个对象都具有同样类型的接口,即系统调用。就像每个文件都有一个打开文件号一样,每个对象也都有唯一的识别号,进程可以通过系统调用传递的识别号来存取这些对象,与文件的存取一样,对这些对象的存取也要验证存取权限,System
V IPC 可以通过系统调用对对象的创建者设置这些对象的存取权限。在Linux 内核中,System V IPC 的所有对象有一个公共的数据结构pc_perm 结构,它是IPC 对象的权限描述,在linux/ipc.h 中定义如下:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
 
struct ipc_perm
{
    key_t key; /* 键 */
    ushort uid; /* 对象拥有者对应进程的有效用户识别号和有效组识别号 */
    ushort gid;
    ushort cuid; /* 对象创建者对应进程的有效用户识别号和有效组识别号 */
    ushort cgid;
    ushort mode; /* 存取模式 */
    ushort seq; /* 序列号 */
};
在这个结构中,要进一步说明的是键(key)。键和识别号指的是不同的东西。系统支持两种键:公有和私有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到System
V IPC 对象的识别号。如果键是私有的,则键值为0,说明每个进程都可以用键值0 建立一个专供其私用的对象。注意,对System V IPC 对象的引用是通过识别号而不是通过键。
(一)、信号量
Linux 中信号量是通过内核提供的一系列数据结构实现的,这些数据结构存在于内核空间,对它们的分析是充分理解信号量及利用信号量实现进程间通信的基础,下面先给出信号量的数据结构(存在于include/linux/sem.h
中)
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
(1)系统中每个信号量的数据结构(sem)
struct sem
{
    int semval; /* 信号量的当前值 */
    unsigned short  semzcnt;  /* # waiting for zero */
    unsigned short  semncnt;  /* # waiting for increase */
    int sempid; /*在信号量上最后一次操作的进程识别号*/
};

(2)系统中表示信号量集合(set)的数据结构(semid_ds)
struct semid_ds
{
    struct ipc_perm sem_perm; /* IPC 权限 */
    long sem_otime; /* 最后一次对信号量操作(semop)的时间 */
    long sem_ctime; /* 对这个结构最后一次修改的时间 */
    struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
    struct sem_queue *sem_pending; /* 待处理的挂起操作*/
    struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */
    struct sem_undo *undo; /* 在这个数组上的undo 请求 */
    ushort sem_nsems; /* 在信号量数组上的信号量号 */
};

(3)系统中每一信号量集合的队列结构(sem_queue)
struct sem_queue
{
    struct sem_queue *next;  /* 队列中下一个节点 */
    struct sem_queue **prev;  /* 队列中前一个节点, *(q->prev) == q */
    struct wait_queue *sleeper;  /* 正在睡眠的进程 */
    struct sem_undo *undo;  /* undo 结构*/
    int pid; /* 请求进程的进程识别号 */
    int status; /* 操作的完成状态 */
    struct semid_ds *sma;  /*有操作的信号量集合数组 */
    struct sembuf *sops;  /* 挂起操作的数组 */
    int nsops; /* 操作的个数 */
};

 C++ Code 
1
2
3
4
5
6
 
struct sembuf
{
    ushort sem_num; /* 在数组中信号量的索引值 */
    short sem_op; /* 信号量操作值(正数、负数或0) */
    short sem_flg; /* 操作标志,为IPC_NOWAIT 或SEM_UNDO*/
};
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
如果进程被挂起,Linux 必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux 内核在堆栈中建立一个 sem_queue 结构并填充该结构。新的 sem_queue 结构添加到集合的等待队列中(利用
sem_pending 和 sem_pending_last 指针)。当前进程放入sem_queue 结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。
当某个进程修改了信号量而进入临界区之后,却因为崩溃或被“杀死(kill)”而没有退出临界区,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux
通过维护一个信号量数组的调整列表(semadj)来避免这一问题。其基本思想是,当应用这些“调整”时,让信号量的状态退回到操作实施前的状态。
(二)、消息队列
Linux 中的消息可以被描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC 的标识号唯一地标识。Linux 为系统中所有的消息队列维护一个 msgque 链表,该链表中的每个指针指向一个
msgid_ds 结构,该结构完整描述一个消息队列。
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 
(1)消息缓冲区(msgbuf)
/* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/
struct msgbuf
{
    long mtype; /* 消息的类型,必须为正数 */
    char mtext[1]; /* 消息正文 */
};

(2)消息结构(msg)
struct msg
{
    struct msg *msg_next; /* 队列上的下一条消息 */
    long msg_type; /*消息类型*/
    char *msg_spot; /* 消息正文的地址 */
    short msg_ts; /* 消息正文的大小 */
};

(3)消息队列结构(msgid_ds)
/* 在系统中的每一个消息队列对应一个msgid_ds 结构 */
struct msgid_ds
{
    struct ipc_perm msg_perm;
    struct msg *msg_first; /* 队列上第一条消息,即链表头*/
    struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */
    time_t msg_stime; /* 发送给队列的最后一条消息的时间 */
    time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */
    time_t msg_ctime; /* 最后修改队列的时间*/
    ushort msg_cbytes; /*队列上所有消息总的字节数 */
    ushort msg_qnum; /*在当前队列上消息的个数 */
    ushort msg_qbytes; /* 队列最大的字节数 */
    ushort msg_lspid; /* 发送最后一条消息的进程的pid */
    ushort msg_lrpid; /* 接收最后一条消息的进程的pid */
};

(三)、共享内存
与消息队列和信号量集合类似,内核为每一个共享内存段(存在于它的地址空间)维护着一个特殊的数据结构shmid_ds,这个结构在include/linux/shm.h 中定义如下:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
/* 在系统中 每一个共享内存段都有一个shmid_ds 数据结构. */
struct shmid_ds
{
    struct ipc_perm shm_perm; /* 操作权限 */
    int shm_segsz; /* 段的大小(以字节为单位) */
    time_t shm_atime; /* 最后一个进程附加到该段的时间 */
    time_t shm_dtime; /* 最后一个进程离开该段的时间 */
    time_t shm_ctime; /* 最后一次修改这个结构的时间 */
    unsigned short shm_cpid; /*创建该段进程的 pid */
    unsigned short shm_lpid; /* 在该段上操作的最后一个进程的pid */
    short shm_nattch; /*当前附加到该段的进程的个数 */
    /* 下面是私有的 */
    unsigned short shm_npages; /*段的大小(以页为单位) */
    unsigned long *shm_pages; /* 指向frames -> SHMMAX 的指针数组 */
    struct vm_area_struct *attaches; /* 对共享段的描述 */
};
我们用图 7.4 来表示共享内存的数据结构shmid_ds 与其他相关数据结构的关系。
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
某个进程第1 次访问共享虚拟内存时将产生缺页异常。这时,Linux 找出描述该内存的vm_area_struct 结构,该结构中包含用来处理这种共享虚拟内存段的处理函数地址。共享内存缺页异常处理代码对shmid_ds
的页表项表进行搜索,以便查看是否存在该共享虚拟内存的页表项。如果没有,系统将分配一个物理页并建立页表项,该页表项加入 shmid_ds 结构的同时也添加到进程的页表中。这就意味着当下一个进程试图访问这页内存时出现缺页异常,共享内存的缺页异常处理代码则把新创建的物理页给这个进程。因此说,第1
个进程对共享内存的存取引起创建新的物理页面,而其他进程对共享内存的存取引起把那个页添加到它们的地址空间。
当某个进程不再共享其虚拟内存时,利用系统调用将共享段从自己的虚拟地址区域中移去,并更新进程页表。当最后一个进程释放了共享段之后,系统将释放给共享段所分配的物理页。
当共享的虚拟内存没有被锁定到物理内存时,共享内存也可能会被交换到交换区中。
四、Posix 的IPC 机制
信号量:分为命名和匿名信号量。命名信号量通常用于不共享内存的进程之间(内核实现);匿名信号量可以用于线程通信(存放于线程共享的内存,如全局变量),或者用于进程间通信(存放于进程共享的内存,如System V/ Posix 共享内存)。
消息队列、共享内存:与System V 类似。
互斥锁mutex + 匿名信号量:线程通信
互斥锁mutex + 条件变量condition :线程通信
五、进程池的实现思路
    1、父子进程之间使用管道通信,传递任务数据状态等,主进程使用某种算法主动选择子进程。最简单、最常用的算法是随机算法和Round-Robin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作进程中更均匀地分配、从而减轻服务器的整体压力。
   
2、主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程获得新任务的“接管权”,它可以从工作队列中取出并执行之,而其他子进程将继续睡眠在工作队列上。
进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
六、线程池的实现思路
       主线程接到任务时将任务添加到全局共享的队列中(可以用链表实现),添加任务前后需要加锁,添加任务内部操作成功时触发成功全局共享的信号量或者条件变量,此时所有子线程都在等待可用信号,wait 成功的线程从任务队列取走任务去执行。

进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)的更多相关文章

  1. Linux 进程间通信(管道、共享内存、消息队列、信号量)

           进程通信 : 不同进程之间传播或交换信息    为什么要进程通信呢? 协同运行,项目模块化 通信原理 : 给多个进程提供一个都能访问到的缓冲区. 根据使用场景,我们能划分为以下几种通信 ...

  2. php中对共享内存,消息队列的操作

    http://www.cnblogs.com/fengwei/archive/2012/09/12/2682646.html php作为脚本程序,通常生命周期都很短,如在web应用中,一次请求就是ph ...

  3. 《linux程序设计》--读书笔记--第十四章信号量、共享内存和消息队列

    信号量:用于管理对资源的访问: 共享内存:用于在程序之间高效的共享数据: 消息队列:在程序之间传递数据的一种简单方法: 一.信号量 临界代码:需要确保只有一个进程或者一个执行线程可以进入这个临界代码并 ...

  4. 【C】——信号量 互斥锁 条件变量的区别

    信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在哪里).而互斥锁是用在多线程多任务互斥的,一个线程占用了某 ...

  5. Android线程间通信机制——深入理解 Looper、Handler、Message

    在Android中,经常使用Handler来实现线程间通信,必然要理解Looper , Handler , Message和MessageQueue的使用和原理,下面说一下Looper , Handl ...

  6. C实现进程间通信(管道; 共享内存,信号量)

    最近学习了操作系统的并发:以下是关于进程间实现并发,通信的两个方法. 例子: 求100000个浮点数的和.要求: (1)随机生成100000个浮点数(父进程). (2)然后创建4个后代进程,分别求25 ...

  7. 撸代码--类QQ聊天实现(基于linux 管道 信号 共享内存)

    一:任务描写叙述 A,B两个进程通过管道通信,像曾经的互相聊天一样,然后A进程每次接收到的数据通过A1进程显示(一个新进程,用于显示A接收到的信息),A和A1间的数据传递採用共享内存,相应的有一个B1 ...

  8. linux后台查看共享内存和消息队列的命令

    ipcs ipcs -q : 显示所有的消息队列 ipcs -qt : 显示消息队列的创建时间,发送和接收最后一条消息的时间 ipcs -qp: 显示往消息队列中放消息和从消息队列中取消息的进程ID ...

  9. Linux系统编程之命名管道与共享内存

    在上一篇博客中,我们已经熟悉并使用了匿名管道,这篇博客我们将讲述进程间通信另外两种常见方式--命名管道与共享内存. 1.命名管道 管道是使用文件的方式,进行进程之间的通信.因此对于管道的操作,实际上还 ...

随机推荐

  1. HDU 1174 爆头(计算几何)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1174 解题报告:就是用到了三维向量的点积来求点到直线的距离,向量(x1,y1,z1)与(x2,y2,z ...

  2. 记录一次MVC 3.0错误 HTTP 404您正在查找的资源(或者它的一个依赖项)可能已被移除,或其名称已更改,或暂时不可用。请检查以下 URL 并确保其拼写正确。

    在部署到IIS7时,MVC3报了一个找不到资源的错误,文件肯定是有的,而且页面是肯定报错的,也就说内部运行错误了,而MVC把错误没有抛出来而已: 所以对症下药,发觉我的项目里面用了rexs进行多语言, ...

  3. 浅析ODS与EDW关系(转载)

    浅析ODS与EDW 关系 刘智琼 (中国电信集团广州研究院广州510630) 摘要 本文重点介绍了企业运营数据仓储(ODS)和企业数据仓库(EDW )的概念,并对ODS与EDW 之间的关系,包括两者相 ...

  4. 【转】 iOS开发UI篇—UIScrollView控件实现图片轮播

    原文:http://www.cnblogs.com/wendingding/p/3763527.html iOS开发UI篇—UIScrollView控件实现图片轮播 一.实现效果 实现图片的自动轮播 ...

  5. Linux2.6内核--对块IO层操作的讨论

          当一个块被调入内存时(也就是说,在读入后或等待写出时),它要存储在缓冲区中.每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示.块包含一个或多个扇区,但大小不能超过一页,所以一页可以容 ...

  6. javascript 正则介绍

    1.正则直接量字符 \o NUL字符(\u000)\t 制表符\n 换行符(\u000A)\v 垂直制表符\f 换页符\xnn 由16进制nn指定的拉丁字符\uXXXX 由16进制XXXX指定的unc ...

  7. 两种高性能 I/O 设计模式 Reactor 和 Proactor

    两种高性能 I/O 设计模式 Reactor 和 Proactor Reactor 和 Proactor 是基于事件驱动,在网络编程中经常用到两种设计模式. 曾经在一个项目中用到了网络库 libeve ...

  8. HDU 2298 Toxophily(公式/三分+二分)

    Toxophily Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

  9. XSS攻击(出现的原因、预防措施......)

    验证XSS攻击重点不是去查找可输入哪些内容会出现什么样的bug就是测试XSS攻击,重点是了解它出现的原理,为什么会出现XSS攻击,导致一些问题出现?如何防御与解决XSS攻击?以下我将简单介绍以上提出的 ...

  10. Python——模块——fnmatch(文件名对比)

    一.模块作用 fnmatch 模块主要用于文件名的比较,使用 Unix shell 使用的 glob 样式模式. 二.简单匹配 fnmatch() 将单个文件名与模式进行比较并返回布尔值,来看它们是否 ...