进程间通信机制(IPC)--信号

进程通信的概念及目的

进程间通信就是在不同进程间传播或交换信息,在linux下的系统编程中,父进程可以通过fork()系统调用来创建一个子进程,之后他们将运行各自的程序代码。一般而言进程的用户空间相互独立,不能互相访问,可如果多个进程间需要协同处理某个任务时这就需要操作系统进程间的同步和数据交流
  进程间通信的目的:

1、数据传输:一个进程需要将它的数据发送给另一个进程。
2、通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
3、资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
4、进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

 常见的进程通信如下:

信号(Signal)

  信号为Linux系统中用于进程间通信或操作中的一种机制,信号可以在任何过程中发送给某个进程,收到信号后,进程不管执行到什么位置,都要暂停去处理信号,处理完毕后在继续执行。信号与硬件中的中断类似,是异步通信模式,早期被称为”软中断“如果一个信号被进程设置为阻塞,则该信号传递被延迟,直到取消后再继续传给进程。
  我们可通过kill -l来查看系统所支持的信号种类,常用的几个信号如下:

  • SIGINT : 终止进程,通常我们的Ctrl+C就发送的这个消息。
  • SIGQUIT:和SIGINT类似, 但由QUIT字符(通常是Ctrl- / )来控制. 进程收到该消息退出时会产生core文件。
  • SIGKILL:消息编号为9,我们经常用kill -9来杀死进程发送的就是这个消息,程序收到这个消息立即终止,这个消息不能被捕获,封锁或这忽略。
  • SIGTERM:是不带参数时kill默认发送的信号,默认是杀死进程。
  • SIGSTOP:停止进程的执行,同SIGKILL一样不可以被应用程序所处理,注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行。
  • SIGCONT:当SIGSTOP发送到一个进程时,通常的行为是暂停该进程的当前状态。如果发送SIGCONT信号,该进程将仅恢复执行。
  • SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。

信号的名词

  • 规则信号(regular signal,编号1-31),也称为不可靠信号:无论发送多少次,在接收进程处理之前,重复的信号会被合并为一个,信号可能会丢失。

  • 实时信号(real-time signal,编号32-63),也称为可靠信号:发送多次,就会在接收进程的信号队列中出现多少次。

  • 信号集即信号的集合,为了批量管理信号在linux中,它的类型是sigset_t,大小是64bits。 因为目前linux流行版本一共有64个信号,一个bit表示一个信号,例如第一位就是1号信号(SIGHUP),第二位就是2号信号(SIGINT)….

  • 抵达(delivery) && 未决(pending)
    抵达:执行信号的处理动作叫抵达,也就是信号被进程接收。抵达通常包括:忽略,执行默认动作,执行处理函数
    未决:就是未抵达,通常指称信号的阻塞为该信号的未决状态。

  • 信号屏蔽状态字(block) && 信号未决状态字(pending)
      在进程控制块(PCB)中的结构体内比较重要的三个变量:信号屏蔽状态字信号未决状态字是否忽略标志
    信号屏蔽状态字(block):64bits,每一位代表该进程对对应号码的信号是否屏蔽:1是屏蔽,0是不屏蔽 。
    信号未决状态字(pending):64bits,每一位代表该进程对对应号码的信号的状态:1是未决,0是不抵达

      当然这些变量间也存在一些联系,例如将进程9的信号屏蔽字设为1,即将SIGKILL信号设为屏蔽状态,此时我给该进程执行kill指令则改进好必然处于未决状态,即信号未决状态字的9号将变为1。

信号API介绍

信号集操作函数

POSIX.1定义了一个数据类型sigset_t,用于表示信号集,在signal.h下提供了五个处理信号集的函数:

  • sigemptyset 初始化set所指向的信号集,清除里面所有已经注册的信号,即将所有位置0 int sigemptyset(sigset_t *set);,若成功则返回0,若出错则返回-1
  • sigfillset 初始化由 set 指向的信号集,使其包含所有信号。即将所有位置1 ,int sigfillset(sigset_t *set);,若成功则返回0,若出错则返回-1
  • sigaddset 将一个信号 signo 添加到现有信号集 set 中。即将该信号对应的位置1int sigaddset(sigset_t *set, int signo);,若成功则返回0,若出错则返回-1
  • sigdelset 将一个信号 signo 从信号集 set 中删除。即将该信号对应的位置0 ,int sigaddset(sigset_t *set, int signo);,若成功则返回0,若出错则返回-1
  • sigismember 判断指定信号 signo 是否在信号集 set 中。 int sigismember(const sigset_t *set, int signo);若真则返回1,若假则返回0,若出错则返回-1。

信号的安装

  linux主要有两个函数实现信号的安装:signal()sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数,有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。
signal()注册一个信号捕捉函数

#include <signal.h>  //头文件

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

第一个参数signum指定信号的值,第二个参数handler制定针对前面函数的处理,可忽略该信号(参数设为SIG_IGN),可以采用系统默认方式处理信号(参数设为SIG_DFL),也可以自己实现处理方式(参数指定一个函数地址),即当指定信号到底时,就会跳转到handler指定的函数执行。
返回值signal()调用成功时,返回最后一次为安装信号signum而调用signal()时的handler值,失败返回SIG_ERR。

sigaction()函数可对发来的信号做排队处理(通常在Linux用其来注册一个信号的捕捉函数)

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);

  sigaction函数用来改变进程收到特定信号后的行为。第一个参数signum为要操作的信号,可以为SIGKILL及SIGSTOP外的任何一个特定有效的信号(这两个信号定义自己的处理函数,导致安装错误)。第二个参数act为指向sigaction结构体的一个指针,在结构体中指定了对信号新的处理方式,当为NULL时,进程会按缺省的方式对信号处理。第三个参数oldact参数输出先前信号的处理方式,也可指为NULL。若第二个参数和第三个参数都为NULL则该函数用来检查信号的有效性。
  struct sigaction结构体如下:

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}

  sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数。
  sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
  sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。

  • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
  • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
  • SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。若SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。

信号的发送

  发送信号的主要函数有:kill()raise()sigqueue()alarm()setitimer()以及abort()。主要介绍最常用的kill()信号。

#include <sys/types.h>

#include <signal.h>				//头文件

int kill(pid_t pid,int signo)	//函数原型

  该系统调用可以向任何进程或进程组发送信号,参数pid为信号的接收进程

  • pid>0 进程ID为pid的进程
  • pid=0 同一个进程组的进程
  • pid<0 pid!=-1 进程组ID为 -pid的所有进程
  • pid=-1 除发送进程自身外,所有进程ID大于1的进程
    signo为信号值当为NULL时,实际不发送任何信号,但会进行错误检查,可用来检查该进程是否存在,及进程是否有向目标发送信号的权限(非root进程执行想属于同一个session或者同一个用户进程发送信号)不为空时可以是系统中的发送信号名称
      函数执行成功时返回值为0,错误时返回-1,并设置相应的错误码errno常用的错误如下:
  • EINVAL:指定的信号sig无效。
  • ESRCH:参数pid指定的进程或进程组不存在。(进程表中包括未被回收的僵尸进程)。
  • EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。

其他相关API

sigprocmask()函数,用来设置、清除、查看信号的屏蔽字。

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

返回值:成功返回0,错误返回-1

  how有三个值可选:

  • SIG_BLOCK: 将set信号集中的信号全部设为阻塞。
  • SIG_UNBLOCK: 解除对set中信号的阻塞。
  • SIG_SETMASK: 把set信号集里面的信号全部设置为阻塞或者解除阻塞。

set为要设置的屏蔽字信号集合,oset为设置当前的进程屏蔽字集合。

sigpending()获取当前进程所有未决的信号。通过其 set 参数返回未决的信号集。

#include <signal.h>
int sigpending(sigset_t *set);

返回值:成功返回0,错误返回-1

  一旦调用sigpending函数,那么set信号集中,处于未决状态的信号对应的位,被置为1

信号集示例代码

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

void handler()
{
    printf("信号解除阻塞\n");
    return;
}

int main()
{
    char tmp = 'a';
    sigset_t bset; //用来设置阻塞的信号集

    sigemptyset(&bset); //清空信号集
    sigaddset(&bset, SIGINT); //将SIG_INT信号添加到信号集中

    if(signal(SIGINT, handler) == SIG_ERR)//注册安装处理函数
        perror("signal err:");

    sigprocmask(SIG_BLOCK, &bset, NULL); //阻塞SIG_INT信号

    while(tmp != 'q' )
    {
        tmp = getchar();
    }
    sigprocmask(SIG_UNBLOCK, &bset, NULL);//解锁阻塞

    pause();
    return 0;

  下运行结果如下:

panghu@Ubuntu-14:~$ ./signal1    
^C^C^C^C^C
q
SIG_INT respond
^CSIG_INT respond

  输入ctrl+c,此时并未看见执行信号处理函数,输入’q’,退出循环,再按ctrl+c,看见控制端打印SIG_INT respond 。

signal()函数示例代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

int child_run = 0;		//子进程运行标志位
int parent_run = 0;		//父进程运行标志位

void sig_child(int signum)
{
    if(SIGUSR1 == signum)
    {
        child_stop = 1;
    }
}
void sig_parent(int signum)
{
    if(SIGUSR2 == signum)
    {
        parent_run = 1;
    }
}
int main(int argc, char **argv)
{
    int         pid;
    int         wstatus;
    signal(SIGUSR1, sig_child);		//注册新号和指定处理函数
    signal(SIGUSR2, sig_parent);

    if((pid = fork()) < 0)    		//创建子进程并判断是否成功
    {
        printf("创建进程失败: %s\n", strerror(errno));
        return -2;
    }
    else if(pid == 0)
    {
        printf("子进程开始运行并且给父进程发送信号\n");
        kill(getppid(), SIGUSR2);	//发送信号给父进程
        while( !child_run )			//子进程阻塞在while中直到父进程发运行信号
        {
            sleep(1);
        }
        printf("子进程收到了父进程发来的信号并且准备推出\n");
        return 0;
    }

    printf("父进程挂起直到收到子进程的信号\n");
    while( !parent_run )			//当子进程发来信号父进程得以运行
    {
        sleep(1);
    }
    printf("父进程开始运行并且发送给子进程一个退出信号\n");
    kill(pid, SIGUSR1);				//发送信号给子进程

    wait(&wstatus);					//避免出现僵尸进程等待子进程退出
    printf("父进程等待子进程退出\n");

    return 0;
}

  运行结果如下:

panghu@Ubuntu-14:~$ ./signal    
父进程挂起直到收到子进程的信号
子进程开始运行并且给父进程发送信号
父进程开始运行并且发送给子进程一个退出信号
子进程收到了父进程发来的信号并且准备推出
父进程等待子进程退出

  需要注意的是:

  • 信号只能被当前运行的进程所接收,信号发送但没有被接收称为挂起信号。
  • 进程可以发送多个信号给一个进程,只不过内核帮接收信号的进程做信号排队处理。
  • 一般在线程设计中我们会用到while( !parent_run )这样的语句代替while(1),好处就是当信号抵达会执行完while里的语句,通常可以作为程序的后续处理语句,但如果是while(1)的话ctrl^c后就不会执行while里的语句,这样的设计可以让程序优雅的退出。

sigaction() 示例代码

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
static void sig_usr(int signum)
{
    if(signum == SIGUSR1)
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}
 
int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数
    
    sigaction(SIGUSR1, &sa_usr, NULL);
    sigaction(SIGUSR2, &sa_usr, NULL);
    
    printf("My PID is %d\n", getpid());
    
    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1)
        {
            if(errno == EINTR)
            {
                printf("read is interrupted by signal\n");
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }
    
    return 0;
}

运行结果:
  程序运行后阻塞等待发送信号:

panghu@Ubuntu-14:~$ ./signal2               
My PID is 19673

  另一个进程发送:

panghu@Ubuntu-14:~$ kill -USR1 19674
panghu@Ubuntu-14:~$ kill -USR2 19674

 收到信号后就会打印:

panghu@Ubuntu-14:~$ ./signal2
My PID is 19674
SIGUSR1 received
read is interrupted by signal
SIGUSR2 received
read is interrupted by signal

巨人的肩膀:
linux中sigaction函数详解
linux信号集与信号屏蔽字

猜你喜欢

转载自blog.csdn.net/weixin_42647166/article/details/104857399
今日推荐