【Linux系统编程学习】信号、信号集以其相关函数

此为牛客Linux C++和黑马Linux系统编程课程笔记。

文章目录

  • 0. 信号的概念
  • 1. Linux信号一览表
  • 2. 信号相关函数
  • 3. kill函数
  • 4. raise函数
  • 5. abort函数
  • 6. alarm函数
  • 7. setitimer函数
  • 8. signal函数
  • 9. 信号集
  • 10. 自定义信号集相关函数
  • 11. sigprocmask函数
  • 12. sigpending函数
  • 13. sigaction函数
  • 14. 内核实现信号捕捉过程

0. 信号的概念

【Linux系统编程学习】信号、信号集以其相关函数_第1张图片
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由内核负责发送的,内核处理。

【Linux系统编程学习】信号、信号集以其相关函数_第2张图片

1. Linux信号一览表

【Linux系统编程学习】信号、信号集以其相关函数_第3张图片
【Linux系统编程学习】信号、信号集以其相关函数_第4张图片
【Linux系统编程学习】信号、信号集以其相关函数_第5张图片
【Linux系统编程学习】信号、信号集以其相关函数_第6张图片
红色为重点掌握的信号

2. 信号相关函数

【Linux系统编程学习】信号、信号集以其相关函数_第7张图片
【Linux系统编程学习】信号、信号集以其相关函数_第8张图片

3. kill函数

#include 
#include 

int kill(pid_t pid, int sig);

功能:给任何的进程或者进程组pid, 发送任何的信号 sig

参数:

  • pid :

< 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)

  • sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

kill(getppid(), 9);能够杀死父进程;kill(getpid(), 9);能够杀死当前进程。

4. raise函数

#include 
#include 

int raise(int sig);

功能:给当前进程发送信号;

参数:sig : 要发送的信号;

返回值:成功 0, 失败 非0。

相当于kill(getpid(), sig);

5. abort函数

#include 
#include 

void abort(void);

功能: 发送SIGABRT(编号为6)信号给当前的进程,杀死当前进程;

相当于kill(getpid(), SIGABRT);raise(SIGBRT);

6. alarm函数

设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。

#include 
unsigned int alarm(unsigned int seconds);

功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM。

参数:seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。

返回值:

  • 之前没有定时器,返回0
  • 之前有定时器,返回之前的定时器剩余的时间

常用:使用alarm(0)取消定时器,返回旧闹钟余下秒数。

每个进程都有且只有唯一个定时器。 比如:进程先执行了alarm(10),2秒后又执行了一个alarm(5),alarm(5)的返回值是8,因为之前有定时器,返回的是之前定时器的剩余时间。然后从现在起该进程还是只有一个定时器,定时5秒,因为后来的定时器会刷新之前的定时器。

注意,alarm定时是与与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。

看以下示例程序:

#include 
#include 
int main()
{
     
    int i;
    alarm(1);
    for(i = 0; ; i++) {
     
        printf("%d\n", i);
    }
    return 0;
}

用定时器让程序执行1s后停止。
我们用time ./alarm来查看该程序的运行时间:
【Linux系统编程学习】信号、信号集以其相关函数_第9张图片
可以看到实际运行时间几乎是1秒,但是发现用户时间和系统时间加起来与总的运行时间不同,这是为什么呢。

实际执行时间 = 系统时间 + 用户时间 + 等待时间。程序的很多时间浪费在printf上了。

7. setitimer函数

#include 
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时。

参数:

  • which : 定时器以什么时间计时,有以下三种参数,一般用第一种自然定时。
    ITIMER_REAL: 真实时间(自然定时),时间到达发送 SIGALRM 常用
    ITIMER_VIRTUAL: 用户时间,时间到达发送 SIGVTALRM
    ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达发送 SIGPROF
  • new_value: 设置定时器的属性
struct itimerval {
           // 定时器的结构体
    struct timeval it_interval;  // 每个阶段的时间,间隔时间
    struct timeval it_value;     // 延迟多长时间执行定时器
};

struct timeval {
             // 时间的结构体
    time_t      tv_sec;     //  秒数     
    suseconds_t tv_usec;    //  微秒    
};
  • old_value :记录上一次的定时的时间参数,一般不使用,指定NULL

如以下示例程序能够实现延迟3秒,每2秒发送一次信号。

#include 
#include 
#include 

// 过3秒以后,每隔2秒钟定时一次
int main() {
     

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;


    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) {
     
        perror("setitimer");
        exit(0);
    }

    getchar();

    return 0;
}

由于还没有介绍signal信号捕捉函数,setitimer发出的信号让程序终止,所以无法演示其周期性发送信号的功能,接下来介绍signal函数。

8. signal函数

#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

功能:设置某个信号的捕捉行为

注意:并不是该函数来捕捉信号,该函数只是向内核注册对某个信号的捕捉行为。

参数:

  • signum: 要捕捉的信号
  • handler: 捕捉到信号要如何处理,可以有以下三种参数:
    - SIG_IGN : 忽略信号
    - SIG_DFL : 使用信号默认的行为
    - 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。

回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

返回值:

  • 成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
  • 失败,返回SIG_ERR,设置错误号

注意:SIGKILL 和 SIGSTOP不能被捕捉,不能被忽略。

在setitimer的示例代码中加入signal后,示例代码如下:

void myfunc(int num) {
     
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {
     

    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM, myfunc);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    if(ret == -1) {
     
        perror("setitimer");
        exit(0);
    }

    getchar();

    return 0;
}

signal的第二个参数传入函数地址,当当前进程捕捉到SIGALRM信号时,将执行程序员自定义的myfunc函数,myfun函数的int类型参数是捕捉到的信号的值(编号)。
程序运行结果如下:
【Linux系统编程学习】信号、信号集以其相关函数_第10张图片
程序运行3秒后第一次发出信号,程序输出一次,然后每隔2秒发出一次信号。

9. 信号集

一个进程的PCB中除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

阻塞信号集(信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)

未决信号集:

  • 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
  • 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
    【Linux系统编程学习】信号、信号集以其相关函数_第11张图片
    信号集本质上是一个64位的二进制数,其每一位的0或1代表着该位序号对应的信号的状态。
    【Linux系统编程学习】信号、信号集以其相关函数_第12张图片
    当信号产生时,PCB中未决信号集中的该位立即置为1,然后去阻塞信号集的同样位置查看是否为1,如果阻塞信号集的对应位置也为1,说明该信号要阻塞,未决信号集的该位置保持1不变;直到阻塞解除,这个信号就被处理。

10. 自定义信号集相关函数

以下信号集相关的函数都是对自定义的信号集进行操作。

int sigemptyset(sigset_t *set);
  • 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
  • 参数:set,传出参数,需要操作的信号集
  • 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
  • 功能:将信号集中的所有的标志位置为1
  • 参数:set:传出参数,需要操作的信号集
  • 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
  • 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
  • 参数:
    - set:传出参数,需要操作的信号集
    - signum:需要设置阻塞的那个信号
  • 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
  • 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
  • 参数:
    - set:传出参数,需要操作的信号集
    - signum:需要设置不阻塞的那个信号
  • 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
  • 功能:判断某个信号是否阻塞
  • 参数:
    - set:需要操作的信号集
    - signum:需要判断的那个信号
  • 返回值:
    1 : signum被阻塞
    0 : signum不阻塞
    -1 : 失败

一个用到以上函数的示例程序如下:

#include 
#include 

int main() {
     

    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if(ret == 0) {
     
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
     
        printf("SIGINT 阻塞\n");
    }

    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
     
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
     
        printf("SIGINT 阻塞\n");
    }

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
     
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
     
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
     
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
     
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}

11. sigprocmask函数

之前的信号集函数均是对自定义的信号集进行操作,那如何修改内核中的阻塞信号集呢?可以使用sigprocmask函数,用自定义的信号集设置内核阻塞信号集。

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)。

参数:

  • how : 如何对内核阻塞信号集进行处理,有以下可选参数:

SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(假设内核中默认的阻塞信号集是mask, mask | set)。

SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞(相当于 mask = mask & ~set)。

SIG_SETMASK: 用set覆盖内核中原来的值。

  • set :已经初始化好的用户自定义的信号集
  • oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL。

返回值: 成功:0 ;失败:-1,并设置错误号。

12. sigpending函数

#include 
int sigpending(sigset_t *set);

功能:获取内核中的未决信号集。

参数:set,传出参数,保存的是内核中的未决信号集中的信息。

13. sigaction函数

sigaction函数通常用于替代signal函数,用来捕捉信号,同时自定义信号的处理动作。

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

功能:检查或者改变信号的处理。信号捕捉。

参数:

  • signum : 需要捕捉的信号的编号或者宏值(信号的名称)
  • act :捕捉到信号之后的处理动作
  • oldact : 上一次对信号捕捉相关的设置,一般不使用,传NULL即可

返回值: 成功 0 失败 -1

其中参数act的类型sigaction结构体定义如下:

struct sigaction {
     
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

其中sa_sigaction和sa_restorer我们基本用不到,所以掌握以下三个即可:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作。
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。

该函数与signal函数最大的区别就在于sa_mask上,sa_mask是程序员自定义的一个信号集,该信号集充当调用信号处理函数时的一个临时的阻塞信号集,也就是说:

进程正常运行时,默认PCB中有一个信号屏蔽字(阻塞信号集),假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。

示例程序如下:

#include 
#include 
#include 
#include 

void catchFunc(int signo) {
     
    printf("捕捉到了信号:%d \n", signo);
}

int main() {
     
    struct sigaction act;
    act.sa_handler = catchFunc;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号

    int res = sigaction(SIGINT, &act, NULL);
    if(res == -1) {
     
        perror("sigaction error");
        exit(1);
    }

    while(1);

    return 0;
}

执行结果如下:
每次在终端输入ctrl+c(产生SIGINT信号)时,输出:捕捉到了信号2。
在这里插入图片描述
当在键盘中输入ctrl+\(产生SIGQUIT)时, 程序退出。那么有个问题,程序中不是已经设置了sigaddset(&act.sa_mask, SIGQUIT);来屏蔽信号了吗?为什么输入ctrl+\时, 程序依然会退出?是因为sigaction函数设置的sa_mask只在信号处理函数执行中生效,输出语句后信号处理函数以及执行完毕。

再看下面示例程序:该程序让信号处理函数睡眠10秒。

#include 
#include 
#include 
#include 
#include 

void catchFunc(int signo) {
     
    printf("捕捉到了信号:%d \n", signo);
    sleep(10);
    printf("-----finish-----");
}

int main() {
     
    struct sigaction act;
    act.sa_handler = catchFunc;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号

    int res = sigaction(SIGINT, &act, NULL);
    if(res == -1) {
     
        perror("sigaction error");
        exit(1);
    }

    return 0;
}

运行程序后,输入ctrl+c,终端输出如下:
在这里插入图片描述
此时10秒以内,依然在执行信号捕捉函数catchFunc,也就是说当前sa_mask是生效的。此时我们输入crtl+\:
在这里插入图片描述
程序并没有退出,因为此时sa_mask中屏蔽了SIGQUIT信号。等待10秒过后:
在这里插入图片描述
发现程序自动退出,这是因为10秒过后信号捕捉函数catchFunc执行完毕,临时的阻塞信号集(sa_mask)失效,此时生效的是原PCB中的阻塞信号集,未决信号集(SIGQUIT处的值为1)查询到后阻塞信号集中SIGQUIT处的值是0后,SIGQUIT信号递达,程序退出。

这里还有一个值得注意的细节:
在这里插入图片描述
当信号捕捉函数catchFunc执行时,我输入了多个ctrl+c后,信号捕捉函数执行完毕后,只输出了一个“捕捉到了信号:2”,这是因为我们无论向当前进程发出多少个相同信号,未决信号集的对应位都是1,无法记录相同信号的数量,所以当临时阻塞信号集被取消后,只输出了一个“捕捉到了信号:2”。有以下结论:

阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)。

14. 内核实现信号捕捉过程

【Linux系统编程学习】信号、信号集以其相关函数_第13张图片

你可能感兴趣的:(Linux,C++学习笔记,#,Linux系统编程篇,linux)