Linux系统编程学习笔记(6)-信号

什么是信号

信号是由单个词组成的消息。例如,当在终端按Ctrl-C时,内核会向此终端运行的进程发送中段信号,信号的来源有三个地方:
(1) 用户:用户可以输入Ctrl-C或者其他信号来更改进程运行情况。
(2) 内核:当程序执行出错时,内核给进程发送一个信号,如溢出等等。同时内核也利用信号通知进程特定事件的发生。
(3) 进程:一个进程可以通过系统调用kill给另一个进程发送信号。一个进程可以和另一个进程通过信号通信。

由进程的某个操作产生的信号被称为同步信号,由进程外的时间引起的信号被称为异步信号,如用户击键这样的操作。信号列表储存在/usr/include/signal.h文件中。

拿信号SIGINT(中段信号)举例,进程接收到此信号时不一定要消亡,通过调用系统函数signal,可以使用其他的方法处理信号

(1) 接收默认处理:默认处理通常为消亡。进程不一定要使用signal接收默认处理,但是进程能够通过以下调用来恢复默认处理:

signal(SIGINT, SIG_DFL);

(2) 忽略信号:程序可以通过以下调用来告诉内核,它需要忽略SIGINT信号

signal(SIGINT, SIG_IGN);

(3) 调用一个函数:调用此系统函数后,程序能告诉内核,当信号到来时并不是忽略,也不执行默认的操作,而是执行函数(类似回调函数),程序调用如下:

signal(signum, functionname);

系统函数signal的调用规则如下:

#include <signal.h>
/***********************
param   signum 需响应的信号
param   action 如何响应
return  -1 遇到错误
        prevaction 成功返回 
***********************/
result = signal(int signum, void(*action)(int));

其中,action可以是函数名或以下的两个特殊值之一:

  • SIG_IGN 忽略信号
  • SIG_DFL 将信号恢复为默认处理

信号与时钟编程

目前最常用的延迟函数为sleep,sleep(n)函数将当前程序挂起n秒或者在此期间被一个不能被忽略的信号的到达所唤醒。

sleep函数的调用有三个步骤组成:

  1. 为信号SIGALARM设置一个处理函数
  2. 调用alarm(num_seconds)
  3. 调用pause

值得注意的是,并非仅有信号SIGALARM可以唤醒进程,任何信号都可以,alarm函数的处理过程如下:

#include <unistd.h>
/*******************************
param   seconds  等待的时间(秒)
return  -1  出错
        old 计时器剩余时间
*******************************/
unsigned old = alarm(unsigned seconds)

如何使用间隔计时器

程序中使用间隔计时器比较麻烦,首先要选择初始间隔和重复间隔。在间隔计时器用的结构体中初始时间是it_value, 重复间隔是it_inteval。如果不想要重复这一特征,将it_inteval设置为0。要把两个时钟都关掉,设置it_value为0.

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void countdown(int signum){
    static int num = 10;
    printf("%d..", num--);
    fflush(stdout);
    if(num < 0){
        printf("DONE! \n");
        exit(0);
    }
}

int set_ticker(int n_msecs){
    struct itimerval new_timeset;
    long n_sec,n_usecs;

    n_sec = n_msecs/1000;
    n_usecs = (n_msecs%1000)*1000L;
    new_timeset.it_interval.tv_sec = n_sec;
    new_timeset.it_intercal.tv_usec = n_usec;

    new_timeset.it_value.tv_sec = n_sec;
    new_timeset.it_value.tv_usec = n_usec;
    return setitmer(ITIMER_REAL, &new_timeset, NULL);
}

int main(){
    signal(SIGALARM, countdown);
    if(set_ticker(500) == -1)
        perror("set_ticker");
    else
        while(1)
    return 0;
}

代码理解起来很简单,设置定时器,到达固定时间后发出SIGALARM信号,函数countdown随之响应。间隔计时器的设置是通过struct itimerval来完成的。这个结构类型包括初始间隔和重复间隔,两者都储存在struct timecal中:

struct itimerval{
    struct timeval it_value;     //初始间隔 
    struct timeval it_interval;  //持续间隔
}
struct timeval{
    time_t tv_sec;      //秒
    suseconds tv_usec;  //微秒
}

设置定时器和取得定时器的系统函数调用:

#include <sys/time.h>
/**********************************
参数:   which     获取或设置的计时器
        val       向当前设置值的指针
        newval    指向要被设置值的指针
        oldval    指向被替换的设置值的指针

返回值:    -1  出错
           0   成功

**********************************/
result = getitimer(int which, struct itimerval *val);
result = setitimer(int which, const struct itimerval *newval, struct itimerval *oldval);

getitimer将某个特定计时器的当前设置读到val指向的结构中。setitimer将计时器设置为newval指向的结构的值。如果oldval不指向null,之前计时器的设定将被复制到oldval指向的结构中。

信号的处理形式

目前使用的信号处理方式有两种,主要是调用的系统函数不同,分为signal与sigaction。

使用signal处理信号

首先看下面的代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#define INPUTLEN 100

int main(int ac, char *av[]){
  void inthandler(int);
  void quithandler(int);
  char input[INPUTLEN];
  int nchars;

  signal(SIGINT, inthandler);
  signal(SIGQUIT, quithandler);

  do{
      printf("\nType a message\n");
      nchars = read(0, input, (INPUTLEN-1));
      if(nchars == -1)
        perror("read returned an error");
      else{
          input[nchars] = '\0';
          printf("You typed: %s", input);
        }
    }
  while(strncmp(input, "quit", 4) != 0);
}

void quithandler(int s){
  printf("Received signal %d.. waiting\n", s);
  sleep(3);
  printf("Leaving quithandler \n");
}

void inthandler(int s){
  printf("Received signal %d.. waiting\n", s);
  sleep(2);
  signal(SIGQUIT, quithandler);
}

在程序运行时,若在输入Ctrl-C之后立马输入Ctrl-|,会发现输出如下:

Type a message
^CReceived signal 2.. waiting   //信号2代表中段信号,Ctrl—C
^\Received signal 3.. waiting   //信号3代表退出信号,Ctrl-|
Leaving quithandler 
Leaving inthandler  

经过上述的实验可以看出,在处理信号INI时,信号QUIT到达,此时,信号INI的处理函数并没有继续执行,而是首先执行的quithandler,等待quithandler执行完成时,再继续执行inthandler.
为了取消这种信号接收的模式,可以修改inthandler函数,使其在处理INT信号时忽略QUIT信号,待INT信号处理完成时恢复的QUIT信号的处理:

void inthandler(int s){
  printf("Received signal %d.. waiting\n", s);
  signal(SIGQUIT, SIG_IGN); //处理信号INT时忽略信号QUIT
  sleep(2);
  printf("Leaving inthandler \n");
  signal(SIGQUIT, quithandler); //信号INT处理完成时恢复对信号QUIT的处理形式
}

使用sigaction处理信号

sigaction系统函数相对于signal的功能更加全面,函数原型如下:

#include <signal.h>
/******************************* ********
param:  signum  要处理的信号
        prevaction  指针,指向描述被替换操作的结构
        action  指针,指向描述操作的结构

return  -1  失败
        0  成功
***************************************/
res = sigaction(int signum, const struct sigaction *action, struct sigaction *prevaction);

参数prevaction可以为null。结构体sigation定义了如何处理一个信号,结构体定义如下

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

新旧方法的差异在于,使用signal函数处理信号时,无法知晓信号的发出源头,而新方法则可以知悉被调用的原因以及上下 文产生的信息,使用时两者之间的差异如下:

struct sigaction action;    action.sa_handler = handler_old;
struct sigaction acton;    action.sa_handler = handler_old;

同时只需设置sa_flags的SA_SIGINFO位即可使用新的信号处理方式,sa_flags的的部分控制位如下:

  • SA_RESTHNAND 当处理函数被调用时重置,此位被设置将导致信号只会被处理一次
  • SA_NODEFER 处理信号时关闭信号自动阻塞,允许递归调用信号处理函数
  • SA_RESRART
  • SA_SIGINFO 如果此位没设置,那么使用sa_handler指向的处理函数的值。如果设置此位,那么传递给处理函数的将不只是信号编号,还将包括指向描述信号产生的原因和条件的结构体。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#define INPUTLEN 100

void inthandler(int s){
  printf("Called with signal %d\n", s);
  sleep(s);
  printf("handle done signal %d\n", s);
}

int main(int argc, char *argv[])
{
  struct sigaction newhandler;
  sigset_t blocked;
  char x[INPUTLEN];
  newhandler.sa_handler = inthandler;
  newhandler.sa_flags = SA_RESTART;
  //处理阻塞的信号
  sigemptyset(&blocked);
  sigaddset(&blocked, SIGQUIT);
  newhandler.sa_mask = blocked;

  if(sigaction(SIGINT, &newhandler, NULL) == -1)
    perror("sigaction");
  else
    while(1 stdin);
      printf("input: %s", x );
    }
}

注意,上述处理阻塞信号的方式,设置sa_mask来设置阻塞的信号类型。当程序执行时连续按下Crtl+C与Ctrl+|程序将执行完INT信号的处理函数之后退出。下面将详细描述如何设置信号为阻塞信号:
使用sigprocmask函数可以设置或取消被阻塞的信号集:

#include <signal.h>
/******************************************
param   how 如何修改阻塞信号集
        sigs 指向使用的信号集的指针
         prev 指向之前的阻塞信号集的指针
return  -1  失败
        0  成功
*******************************************/
int res = sigprocmask(int how, const sigset_t *sigs, sigset_t *prev);

how的值可以为SIG_BLOCK, SIG_UNBLOCK, SIG_SET。表示将sigs指定的信号添加,删除或者替换。
信号集可以用sigsetops函数来进行设置:

sigempty(sigset_t *setp)  //清除列表中的信号
sigfillset(sigset_t *setp)  //添加所有的信号到setp中
sigaddset(sigset *setp, int signum) //添加信号signum到setp信号列表
sigdelset(sigset *setp, int signum) //删除信号

猜你喜欢

转载自blog.csdn.net/qq_34561506/article/details/78819102