Linux进程间通信——信号

一、什么是信号

信号是Linux系统中唯一的异步通信机制,也可以看作是异步通知,通知接收信号的进程有某种事件发生,这类似于DOS下的int或Windows下的事件。信号是在软件层面上对中断机制的一种模拟,进程收到一个信号类似于处理器收到一个中断请求。由于信号是异步的,进程不必等待信号的到达,事实上,它也不知道信号何时会到达。


信号一般是由系统中一些特定事件引起的,主要包括如下。

1.硬件故障

2.程序运行中的错误,例如除数为0,或者访问进程以外的内存区域。

3.进程的子程序终止

4.用户从终端向进程发送终止等信号

5.进程调用kill,raise,以及alarm等函数向其他进程发送信号。


这里需要注意一下:并不是程序中的所有错误都会产生信号。例如,调用库函数时发生的错误,一般都是通过返回值和错误码来报告错误的发生。只有那些在程序的任何位置都可能发生的错误,才会产生信号,报告错误的发生。

进程收到信号后,对于特定的信号,例如SIGKILL和SIGSTOP信号,处理方式是确定的,对于大部分信号,进程可以选择不同的响应方式:

1.捕获信号,这类似于中断处理程序,对于需要处理的信号,进程可以指定相应的函数来进行处理。

2.忽略信号,对信号不进行处理,就像未收到一样,有两个信号不能忽略,即SIGKILL和SIGSTOP信号。

3.让Linux内核执行与信号对于的默认动作,对于大部分信号而言,默认的处理方式是终止相应的程序。

进程可以通过signal或sigaction函数来选择具体的响应方式。

二、信号的类型

Linux系统中,可以使用kill -l命令来列出系统中所有的信号。

Linux进程间通信——信号_第1张图片

其中编号为1-31的信号是从早期的unix系统中继承过来的,由于在传送的过程中可能会丢失,所以也称为不可靠信号或非实时信号。这些信号已经有了确定的用途和含义,并且每种信号都有各自的默认处理方式。例如:用户在键盘上按下Ctrl+C时,会产生SIGINT信号,系统对该信号的默认处理方式就是终止相应的进程。编号为32-64的信号是后来扩充的,它解决了传送过程中可能丢失的问题,称为可靠消息或实时消息,这些信号是POSIX标准的一部分,可用于应用进程。

部分信号类型说明:

1) SIGHUP (挂起) 当运行进程的用户注销时通知该进程,使进程终止 
2) SIGINT (中断) 当用户按下时,通知前台进程组终止进程 
3) SIGQUIT (退出) 用户按下或时通知进程,使进程终止 
4) SIGILL (非法指令) 执行了非法指令,如可执行文件本身出现错误、试图执行数据段、堆栈溢出 
5) SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用 
6) SIGABRT (异常中止) 调用abort函数生成的信号 
7) SIGBUS 非法地址, 包括内存地址对齐(alignment)出错. eg: 访问一个四个字长的整数, 但其地址不是4的倍数.
8) SIGFPE (算术异常) 发生致命算术运算错误,包括浮点运算错误、溢出及除数为0. 
9) SIGKILL (确认杀死) 当用户通过kill -9命令向进程发送信号时,可靠的终止进程 
10) SIGUSR1 用户使用 
11) SIGSEGV (段越界) 当进程尝试访问不属于自己的内存空间导致内存错误时,终止进程 
12) SIGUSR2 用户使用 
13) SIGPIPE 写至无读进程的管道, 或者Socket通信SOCT_STREAM的读进程已经终止,而再写入。 
14) SIGALRM (超时) alarm函数使用该信号,时钟定时器超时响应 
15) SIGTERM (软中断) 使用不带参数的kill命令时终止进程 
17) SIGCHLD (子进程结束) 当子进程终止时通知父进程 
18) SIGCONT (暂停进程继续) 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 
19) SIGSTOP (停止) 作业控制信号,暂停停止(stopped)进程的执行. 本信号不能被阻塞, 处理或忽略. 
20) SIGTSTP (暂停/停止) 交互式停止信号, Ctrl-Z 发出这个信号 
21) SIGTTIN 当后台作业要从用户终端读数据时, 终端驱动程序产生SIGTTIN信号 
22) SIGTTOU 当后台作业要往用户终端写数据时, 终端驱动程序产生SIGTTOU信号 
23) SIGURG 有"紧急"数据或网络上带外数据到达socket时产生. 
24) SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。 
25) SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。 
26) SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. 
27) SIGPROF (梗概时间超时) setitimer(2)函数设置的梗概统计间隔计时器(profiling interval timer) 
28) SIGWINCH 窗口大小改变时发出. 
29) SIGIO(异步I/O) 文件描述符准备就绪, 可以开始进行输入/输出操作. 
30) SIGPWR 电源失效/重启动
31) SIGSYS 非法的系统调用。


三、信号处理函数

1、signal函数

signal函数的一般形式如下:

#include 
void(*signal(int sig,void(*handler)(int)))(int);

参数sig为要设置处理函数的信号;参数handler为指向函数的指针,用来设定信号的处理函数,也可以使用下面某个值:

(1)SIG_IGN:忽略参数sig所指定的信号。

(2)SIG_DFL:采用系统默认方式处理sig所指定信号。

下面编写一个程序,使用signal函数设置SIGINT和SIGQUIT信号的处理函数

//使用signal函数设置sigint和sigquit信号的处理函数
#include 
#include 
#include 
#include 
void sig_handler(int sig)//信号处理函数
{
    switch (sig)
    {
        case 2:         //处理信号SIGINT信号 SIGINT==2  Ctrl+C
            printf("received signal:sigint\n");
            break;
        case 3:         //处理信号SIGQUIT信号 SIGQUIT==3  Ctrl+\
            printf("received signal:sigquit\n");
            break;
        default:
            exit(1);
    }
}
int main()
{
    printf("PID:%d\n",getpid()); //输出进程的标识符
    signal(SIGINT, sig_handler); //设置SIGINT信号的处理函数
    signal(SIGQUIT, sig_handler); //设置SIGQUIT信号的处理函数
    for (; ; ) {
        
    }
    return 0;
}

Linux进程间通信——信号_第2张图片


2.sigaction函数

sigaction函数和signal函数类似,只是它支持信号带有参数,进而可以传递消息给处理函数。该函数的一般形式如下:


#include 
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
参数sig为要设置处理函数的信号;参数act用来设定信号的处理函数,如果为NULL,则系统会以默认方式对信号进行处理;参数oact用来保存信号以前的处理信息,一般情况下设置为NULL即可。下面介绍sigaction结构,其中包含了对指定信号的处理函数,信号所传递的信息,信号处理过程中应当被阻塞其他信号等信息。它的定义如下:


struct sigaction {
          union {
                  __sighandler_t sa_handler;
                  void (*sa_sigaction)(int, struct siginfo *, void *);
          } _u;
          sigset_t sa_mask;
          unsigned long sa_flags;
          void (*sa_restorer)(void);
};

其中 sa_handlersa_sigaction用来设定信号的处理函数,除用户自定义的函数除外,还可以取SIG_DEL或SIG_IGN。sa_handler设定的处理函数只有一个参数,即信号值,所以信号不能传递附加信息,sa_sigaction设定的信号处理函数带有3个参数:第一个参数为信号值,第二个参数为指向siginfo结构的指针,其中可以包含要传递的信息,最后一个参数一般不使用。

sa_mask用来指定在信号处理过程中,哪些信号应当被阻塞;sig_flags中包含许多标志位,其中比较重要的一个为SA_SIGINFO,它表示信号附带的参数可以被传递到信号处理函数之中;sa_restorer现在已不再使用。

注意:如果要传递消息,必须使用sa_sigaction来设定信号的处理函数,同时设置SA_SIGINFO标志位。

例:使用sigaction函数设置SIGINT信号的处理函数。

//使用sigaction函数设置SIGINT信号的处理函数
#include 
#include 
#include 
#include 
void sig_handler(int sig,siginfo_t *info,void *t) //信号处理函数
{
    printf("Receive signal:%d\n",sig);
    return;
}
int main()
{
    int status;
    struct sigaction act;   //定义sigaction结构
    act.sa_sigaction=sig_handler; //使用sa_sigaction来设定处理函数
    sigemptyset(&act.sa_mask);  //清空信号集中的所有信号。
    act.sa_flags=SA_SIGINFO;    //设置SA_SIGINFO标志位
    status=sigaction(SIGINT, &act, NULL); //设置SIGINT信号的处理函数
    if (status<0)
    {
        printf("error\n");
        
    }
    for (; ; ) {
        
    }
    return 0;
}
Linux进程间通信——信号_第3张图片
这里只给出了sigaction函数的基本使用方法,并没有传递信号的附加消息。后面再讲。

四、信号发送函数

Linux系统中最常使用的信号发送函数主要有:kill,raise,alarm以及setitimer.

1.kill 函数

kill函数用于向进程或进程组发送一个信号。

#include 
#include 
int kill(pid_t pid,int sig)

参数: 
pid:可能选择有以下四种

1. pid大于零时,pid是信号欲送往的进程的标识。
2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
4. pid小于-1时,信号将送往以-pid为组标识的进程。

sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。

返回值说明: 成功执行时,返回0。失败返回-1,errno被设为以下的某个值 

EINVAL:指定的信号码无效(参数 sig 不合法) 

EPERM;权限不够无法传送信号给指定进程 

ESRCH:参数 pid 所指定的进程或进程组不存在

2.raise函数

raise函数用于向进程本身发送信号。

#include 
int raise(int sig);
参数sig为要发送的信号值,该函数执行成功后,返回值为0,否则返回-1.

3.abort函数

abort函数用于向进程发送SIGABORT信号,默认情况下进程会异常退出,当然,用户也可以定义自己的信号处理函数。

#include 
void abort(void);
该函数没有参数和返回值。

4.sigqueue函数

sigqueue函数用于向进程发送信号,同时还支持附加信息的传递。

#include 
int sigqueue(pid_t pid, int sig, const union sigval value);
参数:
sigqueue的第一个参数是指定接收信号的进程id,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的附加信息。

返回值:成功返回0,失败返回-1。

联合数据结构union sigval:

typedef union sigval
{
    int sival_int;
    void *sival_ptr;
}sigval_t;
调用sigqueue函数时,sigval_t所指定的信息将会被复制到前面介绍的sa_sigaction的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。

例:在不同进程间实现信号发送和接收,同时在传递过程中附加其它信息。

//在不同的进程间实现信号发送和接收,同时在传递过程中附加其他信息(发送进程)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int status;
    pid_t pid;
    union sigval sg;
    printf("发送进程PID:%d\n",getpid());
    printf("输入接收进程的PID:");
    scanf("%d",&pid);
    sg.sival_int=getpid(); //获取当前进程的标识符,作为附加信息发送出去
    status=sigqueue(pid,SIGUSR1,sg); //发送信号
    if (status<0)
    {
        printf("send error\n");
    }
    else
    {
        printf("Done\n");
    }
    return 0;
}


//在不同的进程间实现信号发送和接收,同时在传递过程中附加其他信息(接收进程)
#include 
#include 
#include 
#include 
void sig_handler(int sig,siginfo_t *info,void *t) //信号处理函数
{
    printf("\nreceive signal:%d\n",sig); //输出收到的信号值
    printf("receive message:%d\n",info->si_pid); //输出接收到的附加信息,这里是发送进程的PID
    
}
int main()
{
    int status;
    pid_t pid;
    struct sigaction act;     //定义sigaction结构
    pid=getpid();             //获取当前进程的标志符
    printf("接收进程的PID:%d",pid);
    act.sa_sigaction=sig_handler; //使用sa_sigaction来设置信号处理函数
    sigemptyset(&act.sa_mask); //清空信号集中的所有信号
    act.sa_flags=SA_SIGINFO;  //设置SA_SIGINFO标志位
    status=sigaction(SIGUSR1, &act, NULL);//设置SIGUSR1信号的处理函数
    if (status<0)
    {
        printf("sigaction error\n");
    }
    printf("\nreceiver:\n");
    
    for (; ; ) {
        
    }
    return 0;
}

Linux进程间通信——信号_第4张图片

5.alarm函数

alarm函数用于在系统中设置一个定时器,计时到达后向进程发送SIGALRM信号。

#include 
unsigned int alarm(unsigned int seconds);
如果在上一次调用alarm函数所设定的时间到达之前,再次调用alarm函数,则函数的返回值为上一次设定的剩余时间,同时,新设定的时间将取代原来的时间。

//使用alarm函数在系统中设置一个定时器
#include 
#include 
#include 
int main()
{
    int i;
    alarm(1); //设置定时器
    i=0;
    while (1)
    {
        printf("i= %d\n",i);
        sleep(0.5);
        i++;
    }
    return 0;
}

6.setitimer和getitimer函数

setitimer函数用来设置定时器,getitimer函数用来读取定时器的状态。

#include 
#include 
int getitimer(int which,struct itimerval *value);
int setitimer(int which,const struct itimerval *value,struct itimerval *ovalue);
其中which参数表示类型,可选的值有:

ITIMER_REAL:以系统真实的时间来计算,计时到达后向进程送出SIGALRM信号。
ITIMER_VIRTUAL:仅在进程执行时(用户态)计时,计时到达后送出SIGVTALRM信号。
ITIMER_PROF:以该进程在用户态下和内核态下(系统调用)所费的时间来计算,它送出SIGPROF信号。
参数value为设定的时间,value为上次调用设定的时间,函数执行成功后,返回0;执行过程中遇到错误返回-1,并设置相应的错误码。

EFAULT:参数value或者value是一个无效的指针。

        EINVAL: 参数which错误。

value和ovalue均为itimerval结构体:

struct itimerval {
    struct timeval it_interval; /* 下次超时时间 */
    struct timeval it_value;    /* 当前超时时间 */
};

struct timeval {
    time_t      tv_sec;         /* 秒 */
    suseconds_t tv_usec;        /* 微秒 */
};

itimeval又是由两个timeval结构体组成,timeval包含tv_sec和tv_usec两部分,其中tv_se为秒,tv_usec为微秒(即1/1000000秒) 其中的new_value参数用来对计时器进行设置,it_interval为计时间隔,it_value为延时时长。

settimer工作机制是,先对it_value倒计时,当it_value为零时触发信号,然后重置为it_interval,继续对it_value倒计时,一直这样循环下去。

基于此机制,setitimer既可以用来延时执行,也可定时执行。若it_interval的值为0,则定时器超时后不再重新自动启动

假如it_value为0是不会触发信号的(定时器将停止工作并且也不会重新自动启动),所以要能触发信号,it_value得大于0;如果it_interval为零,只会延时,不会定时(也就是说只会触发一次信号)。

ovalue参数,通常用不上,设置为NULL,它是用来存储上一次setitimer调用时设置的value值。

以下是一个简单的使用例子:

#include 
#include 
#include 
#include 

void signalHandler(int signo)
{
    switch (signo){
        case SIGALRM:
            printf("Caught the SIGALRM signal!\n");
            break;
    }
}

int main(int argc, char *argv[])
{
    signal(SIGALRM, signalHandler);
    
    struct itimerval new_value, old_value;
    new_value.it_value.tv_sec = 5;  //5秒钟后将启动定时器
    new_value.it_value.tv_usec = 1;
    new_value.it_interval.tv_sec = 1;  //定时器启动后,每隔1秒将执行相应的函数
    new_value.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &new_value, &old_value);
    
    for(;;);
    
    return 0;
}

五、信号集和信号集操作函数

我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程。那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?信号集函数就是帮助我们解决这些问题的。

下面是信号函数集:

1、int sigemptyset(sigset_t *set);
该函数的作用是将信号集初始化为空。

2、int sigfillset(sigset_t *set);
该函数的作用是把信号集初始化包含所有已定义的信号。

3、int sigaddset(sigset_t *set, int signo);
该函数的作用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。

4、int sigdelset(sigset_t *set, int signo);
该函数的作用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1.

5、int sigismember(sigset_t *set, int signo);
该函数的作用是判断给定的信号signo是否是信号集中的一个成员,如果是返回1,如果不是,返回0,如果给定的信号无效,返回-1;

6、int sigpromask(int how, const sigset_t *set, sigset_t *oset);
该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。
参数how决定函数具体的操作方式:

SIG_BLOCK:将信号集set添加到当前进程的阻塞集合中。

SIG_UNBLOCK:从当前进程的阻塞集合中删除信号集set中的信号,即删除信号集set中的信号,即解除该信号的阻塞。

SIG_SETMASK:将信号集set设置为信号阻塞集合。


#include 
#include 
#include 
#include 
void sig_handler(int sig)//信号处理函数
{
    printf("received signal:SIGINT\n");
    return;
}

int  main()
{
    
    sigset_t set;  //定义信号集
    sigemptyset(&set); //初始化信号集,清空所有信号
    sigaddset(&set, SIGINT); //将SIGINT信号添加到信号集中
    signal(SIGINT, sig_handler); //设置SIGINT信号的处理函数
    while (1)
    {
        sigprocmask(SIG_BLOCK, &set, NULL); //阻塞信号
        printf("SIGINT is blocked\n");
        sleep(5);
        sigprocmask(SIG_UNBLOCK, &set, NULL); //解除阻塞
        printf("SIGINT is unblocked\n");
        sleep(5);
    }
    return 0;
}


#include 
#include 
#include 
#include 
#include 

//倒计时值设置,重复到时,超时值设为1秒
struct itimerval val_alarm={.it_interval.tv_sec=1,.it_interval.tv_usec=0,.it_value.tv_sec=1,.it_value.tv_usec=0};
//sigalrm信号处理函数
void sig_handler(int sig)
{
    printf("Timer has expired,elapsed %d seconds.\n",val_alarm.it_value.tv_sec);
}
int main()
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler=&sig_handler;
    if(sigaction(SIGALRM, &sa, NULL)==-1)
        perror("Failed to call sigaction");
    //使用初始val_alarm的值启动定时器
    if(setitimer(ITIMER_REAL, &val_alarm, NULL)==-1)
        perror("failed to call setitimer");
    while (1)
    {}
    return 0;
}


下面是信号函数集:
1、int sigemptyset(sigset_t *set);
该函数的作用是将信号集初始化为空。

2、int sigfillset(sigset_t *set);
该函数的作用是把信号集初始化包含所有已定义的信号。

3、int sigaddset(sigset_t *set, int signo);
该函数的作用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。

4、int sigdelset(sigset_t *set, int signo);
该函数的作用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1.

5、int sigismember(sigset_t *set, int signo);
该函数的作用是判断给定的信号signo是否是信号集中的一个成员,如果是返回1,如果不是,返回0,如果给定的信号无效,返回-1;

6、int sigpromask(int how, const sigset_t *set, sigset_t *oset);
该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。‘
参数how决定函数具体的操作方式:
SIG_BLOCK:将信号集set添加到当前进程的阻塞集合之中。
SIG_UNBLOCK:从当前进程的阻塞集合中删除信号集set中的信号,即解除该信号的阻塞。
SIG_SETMASK:将信号集set设置为当前信号阻塞集合。
//使进程在某段时间内阻塞信号集中的信号
#include 
#include 
#include 
#include 
void sig_handler(int sig)//信号处理函数
{
    printf("received signal:SIGINT\n");
    return;
}

int  main()
{
    
    sigset_t set;  //定义信号集
    sigemptyset(&set); //初始化信号集,清空所有信号
    sigaddset(&set, SIGINT); //将SIGINT信号添加到信号集中
    signal(SIGINT, sig_handler); //设置SIGINT信号的处理函数
    while (1)
    {
        getchar();
        sigprocmask(SIG_BLOCK, &set, NULL); //阻塞信号
        printf("SIGINT is blocked\n");
        sleep(5);
        sigprocmask(SIG_UNBLOCK, &set, NULL); //解除阻塞
        printf("SIGINT is unblocked\n");
        sleep(5);
    }
    return 0;
}


你可能感兴趣的:(【操作系统】)