Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

整理自网络

Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)

管道(pipe)

管道可以用于相关进程之间的通信,命名管道克服了管道没有名称的限制。因此,除了管道的功能外,它们还允许无关进程之间的通信。

[En]

Pipes can be used for communication between related processes, and named pipes overcome the limitation that pipes do not have names. Therefore, in addition to the functions of pipes, they also allow communication between unrelated processes.

实现机制:

管道是由内核管理的缓冲区,相当于我们放入内存中的一个笔记。管道的一端连接到进程的输出。这一过程将信息输入管道。管道的另一端连接到进程的输入,该进程取出已放入管道中的信息。缓冲区不需要非常大,并且被设计为循环数据结构,以便可以回收流水线。当管道中没有信息时,从管道读取的进程将等待,直到另一端的进程将信息放入。当管道充满信息时,尝试输入信息的进程将等待,直到另一端的进程取出信息。当两个进程都结束时,管道会自动消失。

[En]

A pipe is a buffer managed by the kernel, which is equivalent to a note we put into memory. One end of the pipe is connected to the output of a process. This process puts information into the pipeline. The other end of the pipe is connected to the input of a process that takes out the information that has been put into the pipe. A buffer does not need to be very large and is designed as a circular data structure so that the pipeline can be recycled. When there is no information in the pipe, the process reading from the pipe waits until the process on the other side puts the information in. When the pipeline is full of information, the process that tries to put in the information waits until the process on the other side takes out the information. When both processes end, the pipeline automatically disappears.

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

实现细节:

Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

关于管道的读写

管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。

当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

·内存中有足够的空间可容纳所有要写入的数据;

·内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

管道的读取过程类似于写入过程。但是,进程可以在没有数据或内存被锁定时立即返回错误消息,而不是阻塞进程,具体取决于文件或管道的打开模式。相反,进程可以在索引节点的等待队列中休眠,等待写进程写入数据。当所有进程都完成管道操作时,管道的索引节点被丢弃,共享数据页被释放。

[En]

The read process of a pipe is similar to the write process. However, a process can return an error message immediately when there is no data or memory is locked, rather than blocking the process, depending on the open mode of the file or pipe. Conversely, the process can sleep in the waiting queue of the index node waiting for the write process to write data. When all processes have completed the pipe operation, the pipe’s index node is discarded and the shared data page is released.

Linux函数原型

#include

int pipe(int filedes[2]);

filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);

filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。

程序实例:

int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if(pipe(fd)  0){                 /* 先建立管道得到一对文件描述符 */
        exit(0);
    }

    if((pid = fork())  0)            /* 父进程把文件描述符复制给子进程 */
        exit(1);
    else if(pid > 0){                /* 父进程写 */
        close(fd[0]);                /* 关闭读描述符 */
        write(fd[1], "\nhello world\n", 14);
    }
    else{                            /* 子进程读 */
        close(fd[1]);                /* 关闭写端 */
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

命名管道(named PIPE)

由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux 中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接

函数原型:

#include
#include

int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );

其中pathname是被创建的文件名称,mode表示将在该文件上设置的权限位和将被创建的文件类型(在此情况下为S_IFIFO),dev是当创建设备特殊文件时使用的一个值。因此,对于先进先出文件它的值为0。

程序实例:

#include
#include
#include
#include

int main()
{
    int res = mkfifo("/tmp/my_fifo", 0777);
    if (res == 0)
    {
        printf("FIFO created/n");
    }
     exit(EXIT_SUCCESS);
}

编译这个程序:

gcc –o fifo1.c fifo

运行这个程序:

$ ./fifo1

用ls命令查看所创建的管道

$ ls -lF /tmp/my_fifo
prwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|

注意:ls命令的输出结果中的第一个字符为p,表示这是一个管道。最后的|符号是由ls命令的-F选项添加的,它也表示是这是一个管道。

FIFO读写规则

1.从FIFO中读取数据: 约定:如果一个进程为了从FIFO中读取数据而阻塞打开了FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作

2.从FIFO中写入数据: 约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

详见:http://blog.csdn.net/MONKEY_D_MENG/article/details/5570468

信号(Signal)

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号
语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又
能够统一对外接口,用sigaction函数重新实现了signal函数)

信号种类

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。大多数信号的默认操作是结束接收信号的进程;然而,一个进程通常可以请求系统采取某些代替的操作,各种代替操作是:

  • 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号 不可以被忽略:SIGKILL,它将结束进程;SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
  • 恢复信号的默认操作。
  • 执行预先安排的信号处理功能。进程可以注册特殊的信号处理函数。当进程接收到信号时,信号处理函数像中断服务例程一样被调用,并且当从信号处理函数返回时,控制权返回到主程序并继续正常执行。
    [En]

    execute a pre-scheduled signal processing function. Processes can register special signal handling functions. When the process receives a signal, the signal processing function is called like an interrupt service routine, and when returned from the signal processing function, the control is returned to the main program and continues to execute normally.*

然而,信号和中断是不同的。中断的响应和处理发生在内核空间,而信号的响应发生在内核空间,而信号处理器的执行发生在用户空间。

[En]

However, signals and interrupts are different. The response and processing of the interrupt occur in the kernel space, while the response of the signal occurs in the kernel space, while the execution of the signal processor occurs in the user space.

那么,什么时候检测和响应信号呢?通常发生在两种情况下:

  • 由于系统调用、中断或异常,当前进程在进入内核空间前夕从内核空间返回到用户空间
    [En]

    the current process returns from kernel space to user space on the eve of entering kernel space due to system calls, interrupts or exceptions*

  • 当前进程在内核休眠后被唤醒时,由于检测到信号,提前返回用户空间。
    [En]

    when the current process is awakened after going to sleep in the kernel, it returns to user space ahead of time due to the detection of a signal.*

函数原型等详见:http://www.cnblogs.com/biyeymyhjob/archive/2012/08/04/2622265.html

信号本质

信号是在软件级别对中断机制的模拟。原则上,进程接收信号的方式与处理器接收中断请求的方式相同。信号是异步的,进程不必通过任何操作等待信号到达。事实上,该过程并不知道信号何时到达。

[En]

A signal is a simulation of the interrupt mechanism at the software level. in principle, a process receives a signal in the same way as a processor receives an interrupt request. The signal is asynchronous, and a process does not have to wait for the signal to arrive through any operation. in fact, the process does not know when the signal will arrive.

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

关于信号处理机制的原理(内核角度)

内核通过在进程所在的进程表项的信号字段中设置与信号相对应的位来向进程发送软中断信号。这里需要补充的是,如果信号被发送到休眠进程,则取决于该进程进入休眠的优先级,如果该进程以可中断的优先级休眠,则该进程被唤醒;否则,只设置进程表中信号字段的对应位,但该进程不被唤醒。这一点很重要,因为进程检查其是否接收到信号的时间是在进程即将从内核状态返回到用户状态时,或者进程即将进入或离开适当的低调度优先级休眠状态时。

[En]

The kernel sends a soft interrupt signal to a process by setting the bit corresponding to the signal in the signal field of the process table item in which the process is located. It should be added here that if the signal is sent to a sleeping process, it depends on the priority at which the process enters sleep, and if the process sleeps at an interruptible priority, the process is awakened; otherwise, only the corresponding bits of the signal field in the process table are set, but the process is not awakened. This is important because the time a process checks to see if it receives a signal is when a process is about to return from kernel state to user state, or when a process is about to enter or leave an appropriate low scheduling priority sleep state.

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态(上面的例子程序中,在步骤5中,解除阻塞后,先打印caught SIGQUIT,再打印SIGQUIT unblocked,即在sigprocmask返回前,信号处理程序先执行),进程在用户态下不会有未处理完的信号。

内核在该进程的上下文中处理该进程接收到的软中断信号,因此该进程必须处于运行状态。如果进程接收到要捕获的信号,则在进程从内核状态返回到用户状态时执行用户定义的函数。并且执行用户定义函数的方法非常巧妙。内核在用户堆栈上创建一个新的层,其中返回地址的值被设置为用户定义处理程序的地址,以便当进程从内核返回到弹出堆栈的顶部时,它返回到用户定义的函数。当函数返回并弹出堆栈顶部时,它将返回到最初进入内核的位置,然后继续运行。原因是用户定义的处理程序不能也不允许在内核状态下执行(如果用户定义的函数在内核状态下运行,用户可以获得任何权限)。

[En]

The kernel processes soft interrupt signals received by a process in the context of that process, so the process must be in a running state. If the process receives a signal to capture, the user-defined function is executed when the process returns to the user state from the kernel state. And the method of executing user-defined functions is very ingenious. The kernel creates a new layer on the user stack, in which the value of the return address is set to the address of the user-defined handler, so that when the process returns to the top of the pop-up stack from the kernel, it returns to the user-defined function. When the function returns and pops out of the top of the stack, it returns to the place where it originally entered the kernel, and then continues to run. The reason for this is that user-defined handlers cannot and are not allowed to execute in kernel state (users can get any permissions if user-defined functions run in kernel state).

在信号的处理方法中有几点特别要引起注意。

第一,在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程在调用signal之前又得 到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在 BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。

第二个要引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上(若系统调用未睡眠而是在运行,根据上面的分 析,等该系统调用运行完毕后再处理信号),这时该信号引起进程作一次longjmp,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回 时,进程就象从系统调用返回一样,但返回了一个错误如-1,并将errno设置为EINTR,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内 核可以自动地重新开始系统调用,或者手如上面所述手动设置重启。

第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不 到进程曾经被唤醒,而是象没有发生过该信号一样。所以能够使pause、sleep等函数从挂起态返回的信号必须要有信号处理函数,如果没有什么动作,可 以将处理函数设为空。

第四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程正常或异常终止时,内核都向其父进程发一个SIGCLD 信号,缺省情况下,父进程忽略该信号,就象没有收到该信号似的,如果父进程希望获得子进程终止的状态,则应该事先用signal函数为SIGCLD信号设 置信号处理程序,在信号处理程序中调用wait。

SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,则 什么也不做。其实wait不一定放在信号处理函数中,但这样的话因为不知道子进程何时终止,在子进程终止前,wait将使父进程挂起休眠。

信号生命周期

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

参考资料:

http://www.cnblogs.com/vamei/archive/2012/10/10/2715398.html

http://bbs.chinaunix.net/thread-1947211-1-1.html

Original: https://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html
Author: as_
Title: Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/10152/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

免费咨询
免费咨询
扫码关注
扫码关注
联系站长

站长Johngo!

大数据和算法重度研究者!

持续产出大数据、算法、LeetCode干货,以及业界好资源!

2022012703491714

微信来撩,免费咨询:xiaozhu_tec

分享本页
返回顶部
最近整理资源【免费获取】:   👉 程序员最新必读书单  | 👏 互联网各方向面试题下载 | ✌️计算机核心资源汇总