基于Ubuntu的Linux学习[信号以及管道]

进程间通信机制

通信原因

在 Linux 系统中,以进程为单位分配和管理资源。由于保护的缘故,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭。但在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项任务,因此要求进程之间必须能够互相通信,从而来共享资源和信息。所以,一个操作系统内核必须提供进程间的通信机制。

进程间通信有如下一些目的:

  1. 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
  2. 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  4. 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
  5. 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

通信机制

进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux 支持多种进程间通信机制,信号和管道是其中的两种。除此之外,Linux 还支持 System V(首次出现的 Unix 版本名)的 IPC 机制,包括共享内存、消息队列、信号量,SOCKET(网络通信)。

linux 进程之间的通信主要有下面几种:

  1. 无名管道(Pipe)及有名管道(named pipe):无名管道可用于具有亲缘关系进程间的通信,有名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  2. 信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。
  3. 消息队列(Message Queue):消息队列是消息的链接表,包括 Posix 消息队列和 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
  4. 共享内存(Shared memory):可以说是最有用的进程间通信方式,是最快的形式。是针对其他通信机制运行效率较低而设计。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
  5. 信号量(Semaphore):主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。
  6. 套接字(Socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

信号

概念

信号是进程在运行过程中,由自身产生或由进程外部发过来的消息(事件)。信号是硬件中断的软件模拟(软中断)。信号是 unix 进程间通信最古老的方法。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

每一个信号用一个整型常量宏表示,以 SIG 开头,比如 SIGCHLD、SIGINT 等,它们在系统头文件中定义:

SIGHUP:  从终端上发出的结束信号;
SIGINT:  来自键盘的中断信号(Ctrl-C);
SIGQUIT: 来自键盘的退出信号(Ctrl-\);
SIGFPE:  浮点异常信号(例如浮点运算溢出);
SIGKILL: 该信号结束接收信号的进程;
SIGALRM: 进程的定时器到期时,发送该信号;
SIGTERM: kill 命令发出的信号;
SIGCHLD: 标识子进程停止或结束的信号;
SIGSTOP: 来自键盘(Ctrl-Z)或调试程序的停止执行信号

信号注册

NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

signal 的第 1 个参数 signum 表示要捕捉处理的信号,第 2 个参数是个函数指针,表示要对该信号进行捕捉处理的函数,该参数也可以是 SIG_DFL(表示交由系统缺省处理,相当于白注册了,恢复默认处理)或 SIG_IGN(表示忽略掉该信号而不做任何处理)。signal 如果调用成功,返回该信号的处理函数的地址,否则返回 SIG_ERR。

sighandler_t 类型的形参(函数指针)指向一个信号处理函数,由 signal 函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号处理函数。该函数只有一个整型参数,表示信号值。

接收处理方式

进程接收到信号以后,可以有如下 3 种选择进行处理:

接收默认处理:接收默认处理的进程通常会导致进程本身消亡。例如连接到终端的进程,用户按下 CTRL+c,将导致内核向进程发送一个 SIGINT 的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号,即终止进程的执行;

忽略信号:进程可以通过代码,显示地忽略某个信号的处理;但是某些信号是不能被忽略的;例如忽略掉终端产生的CTRL+C产生的SIGINT信号:

#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<signal.h>
int main()
{
    signal(SIGINT,SIG_IGN);
    while(1)
        sleep(1);
    return 0;
}

该程序运行起来以后,将 CTRL+C 产生的 SIGINT 信号忽略掉了,所以 CTRL+C 将不再能使该进程终止,要终止该进程,可以向进程发送 SIGQUIT 信号(即组合键 CTRL+\)。或者另外开一个端口,然后执行 ps –aux 查看进程,发现该进程号之后用 kill -9 进程号杀掉该进程。

捕捉信号并处理:进程可以事先注册信号处理函数,当接收到某个信号时,由信号处理函数自动捕捉并且处理该信号。有两个信号既不能被忽略也不能被捕捉,它们是 SIGKILL 和 SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即终止进程。

例如捕捉CTRL+C产生的SIGINT信号:

#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<signal.h>
void fun(int n)
{
    printf("666 n=%d\n",n);
}
int main()
{
    signal(SIGINT,fun);  //也可以写成signal(2,fun)
    while(1)
        sleep(1);
    return 0;
}

该程序运行起来以后,通过按 CTRL+C 将不再终止程序的运行,或者另开一个终端,然后发送消息:kill -2 进程号,可以实现 Ctrl+C 同样的效果。因为 CTRL+C 产生的 SIGINT 信号已经由进程中注册的 SignHandler 函数捕捉了。该程序可以通过 Ctrl+\ 终止,因为组合键 Ctrl+\ 能够产生 SIGQUIT 信号,而该信号的捕捉函数尚未在程序中注册。

3.管道

管道是 linux 进程间通信的一种方式。管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。数据被一个进程读出后,将被从管道中删除,其他读进程将不能再读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。

管道有无名管道和有名管道两种,前者用于父子进程间的通信,后者可用于运行同一系统中的任意两个进程间的通信。

无名管道

无名管道函数原型:

#include<unistd.h>
int pipe(int filedes[2]);

特点:只能在亲缘关系进程间通信(父子或兄弟)、有固定的读端和固定的写端、它是特殊的文件,可以用 read、write 等,只能在内存中存在——通常都是单向的通信。

管道好比一条水管,有两个端口,一端进水,另一端出水。管道也有两个端口,分别是读端和写端,进水可看成数据从写端被写入,出水可看数据从读端被读出。我们分别用 read、write 函数来对管道的读端和写端进行读写,所以必须要知道读写两端分别对应的文件描述符。这两个文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]; 然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管道读端和写端对应的两个文件描述符。即 fds[0]和读端相对应,fds[1]和写端相对应。fds[0]有可读属性,fds[1]有可写的属性。

特性:

  1. 无名管道在进行读写操作时,如果没有数据可读,会导致进程阻塞。
  2. 管道两端的关闭是有先后顺序的,如果先关闭写端则从另一端读数据(空管道)时,read 函数将返回 0,表示管道已经关闭(如果管道不为空则能读到数据)。
  3. 但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。(可以捕捉信号进行验证)

有名管道

命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO 文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个 FIFO 文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然 FIFO 文件的节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

创建、删除 FIFO 文件

1、用函数创建和删除 FIFO 文件

创建 FIFO 文件函数原型:

#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char*pathname,mode_t mode);  //相当于创建一个文件

参数pathname为要创建的FIFO文件的全路径名;参数 mode 为文件访问权限,比如 666 即创建者、与创建者同组的用户、其他用户对该有名管道的访问权限都是可读可写。如果创建成功,则返回 0,否则-1。

删除FIFO文件的函数原型:

#include<unistd.h>
int unlink(const char*pathname);

其实这是一个删除任何文件的函数。参数pathname为FIFO文件的全路径名。

2、用命令创建和删除FIFO文件

mkfifo [OPTION] NAME...  (OPTION就是创建的权限)

用命令mkfifo创建:mkfifo 1.fifo 用命令unlink删除(用此命令不仅能够删除通道文件,而且还会删除链接)

3、管道示例参考代码

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
    int fd[2];
    char str[20];
    char buf[20];
    int ret=pipe(fd);
    int pid=vfork();
    if(ret<0)
    {
        perror("error");
    }
    if(pid>0)
    {
        close(fd[1]);
        while(1)
        {
            read(fd[0],str,20);
            printf("%s\n",str);
        }
        close(fd[0]);
    }
    else if (pid==0)
    {
        sleep(1);
        close(fd[0]);
        while(1)
        {
            gets(buf);
            write(fd[1],buf,sizeof(buf));
        }
        close(fd[1]);
        exit(0);
    }
    return 0;
}

Updated: