07_定时器_信号_信号集_注册信号捕捉_处理捕捉到的信号

代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

1. 信号相关的概念

1.1.Linux中的信号的基本概念

  • Linux操作系统所独有的一种消息机制
  • 简单
  • 可携带的信息量少
  • 可以完成进程间通信
    • 不推荐使用
      • 传递的数据太少, 只能传递一个整形数
      • 信号的优先级太高, 打乱程序的执行顺序

1.2.产生信号的场景

为了理解信号,先从我们最熟悉的场景说起:

  1. 用户输入命令,在Shell下启动一个前台进程。

  2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。

  3. 通过kill命令终止某一个进程。

  4. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。

  5. 正在运行的程序访问了非法内存,发生段错误,进程退出。

# 第1,2种是通过操作终端产生的信号
# 第3中 是执行了linux命令产生了信号
# 第4种, 调用了某些函数产生了信号 -> sleep
# 第5种, 对硬件操作产生的信号
# 当linux信号产生之后, 大部分信号都会终止进程

1.3.察看系统定义的信号列表

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

# 1-31号信号, 在linux下的行为是标准的
# 34-64是给系统预留的, 可以随意指定其功能

# kill -9 9号信号,直接强制杀死进程
# kill -15 15号信号,清理状态后,退出进程执行(非强制退出)

1.4.查看信号的详细信息

$ man 7 signal
1.4.1.信号的默认五种处理动作
$ man 7 signal
Term   终止/结束进程
Ign    信号在进程中被忽略
Core   终止/结束进程, 并生成core文件, 可以通过这个文件进行gdb调试
Stop   暂停进程的运行
Cont   让进程从暂停状态继续运行

Term   Default action is to terminate the process.
Ign    Default action is to ignore the signal.
Core   Default action is to terminate the process and dump core (see core(5)).
Stop   Default action is to stop the process.
Cont   Default action is to continue the process if it is currently stopped.
1.4.2.core文件在默认状态下是不会生成的
$ ulimit -a
core file size          (blocks, -c) 0	# 这个值默认为0, 因此core文件大小永远为0, 就不会产生core文件
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3746
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3746
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
1.4.3.设置core文件大小
$ ulimit -c 1024
1.4.4.core在gdb中的使用
[bu2@localhost day07]$ ulimit -c 1024
[bu2@localhost day07]$ vim 01error.c
[bu2@localhost day07]$ gcc 01error.c  -g
[bu2@localhost day07]$ ./a.out 
Segmentation fault (core dumped)-->段错误,并产生core文件
[bu2@localhost day07]$ 

[bu2@localhost day07]$ ls
01error.c  a.out  core.26632

[bu2@localhost day07]$ gdb a.out 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-114.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/bu2/Desktop/codes/day07/a.out...done.
(gdb) core-file core.26632
[New LWP 26632]
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004004c9 in main () at 01error.c:7
7	   strcpy(buf,"hello world");//会段错误
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64
(gdb) 

信号的几种状态

# 有三种状态
1. 产生: 
2. 未决: 信号产生出来, 但是并没有被处理  
3. 递达: 信号被进程处理完毕, 处理完毕,信号就没有了

没有被处理的是未决,处理完的是递达

2. 信号相关的函数

kill/raise/abort

// kill函数:发送指定信号到指定进程
#include <sys/types.h>
#include <signal.h>		// 主要 的头文件

int kill(pid_t pid, int sig);
参数:
	pid: 指定进程的pid
		>0: 将信号发送给指定进程    --> 会使用这个参数就可以了
		==0: 发送给当前进程组中的所有进程
		-1: 将信号发送给所有有权限接收这个信号的进程
		-pid: 这个pid是某个进程组的ID, 将这个信号发送给某个进程组中所有的进程
	sig: 要发送的信号
//raise函数:给自己发送信号
// 参数是要发送的信号, 函数被那个进程调用, 信号就发送给那个进程
#include <signal.h>
int raise(int sig);

kill(getpid(), sig);//kill函数实现raise()的功能
// 调用该函数的进程会收到一个信号: SIGABRT(6号信号), 这个信号会终止当前进程,并产生core文件。6号信号
#include <stdlib.h>
void abort(void);

kill(getpid(), SIGABRT);//kill函数实现abort()的功能

alarm -> 定时器函数

#include <unistd.h>
// 倒计时完毕, 该函数会发出一个信号给当前进程, 信号: SIGALRM 14号信号
// 默认处理动作: 杀死当前进程
// 不能周期性的发送信号
unsigned int alarm(unsigned int seconds);
参数:
	- seconds: 倒计时时长
返回值:
	- 倒计时剩余的时长
	
// 写程序: 1秒可以数多少个数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

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

// 取消定时
int ret = alarm(10);
if(ret == 5)
{
    alarm(0);	// 取消倒计时(参数为0,函数就不工作了)
}
程序实际使用时间
//显示程序执行时间的具体数值
$ time ./a.out
Alarm clock
real	0m1.004s	# 实际用时
user	0m0.103s	# 用户区程序用时
sys	    0m0.338s	# 内核区用时

程序实际使用时间 = 用户区时间 + 内核区时间 + 消耗的时间

//不输出到屏幕:写入到文件中去
[bu2@localhost day07]$ time ./a.out > a
Alarm clock
real	0m1.005s
user	0m0.371s
sys	    0m0.635s

setitimer -> 定时器函数

// setitimer函数:可以周期性定时
// 第一个参数的三种选择,各自发出的一种信号,但都会杀死进程  
// SIGALRM 14号信号
// 因为发出的信号默认杀死进程,所以我们也就看不到周期性的现象了
// 因此,我们需要捕捉信号

//--> 定时器函数,一般和信号捕捉函数配合使用

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
参数:
- which: 定时方式
	- ITIMER_REAL: 真实的使用时间(用户区时间 + 内核区时间 + 消耗的时间) -> 最常用的
		- 计时时间到达发送--定时器信号: SIGALRM
    - ITIMER_VIRTUAL: 只计算程序用户区使用的时间
        - 计时时间到达发送--定时器信号: SIGVTALRM
    - ITIMER_PROF: 只计算内核使用的时间
        - 计时时间到达发送--定时器信号: SIGPROF
- new_value: 要设置的定时的周期
    struct itimerval 
	{
		struct timeval it_interval; /* 定时器发送信号的周期 *//* next value */
		struct timeval it_value;    /* 第一次发出信号的时长 *//* current value */
        // 小明从今晚12点睡觉, 设置闹钟, 明天早晨8点起床, 倒计时8小时 -> 闹钟第一次响的时间
		// 小明因为起不来, 设置了当闹钟第一次响了之后, 每隔5分钟响一次
	};
	// 表示一个时间总和: 秒+微妙
	struct timeval 
	{
		time_t      tv_sec;         /* 秒 *//* seconds */
		suseconds_t tv_usec;        /* 微妙 *//* microseconds */
	};
- old_value: 上一次设置的定时周期, 如果没有需要, 设置为NULL

3. 信号捕捉

3.1 信号捕捉函数

signal函数-(BSD标准)

//signal函数:注册信号捕捉
//捕捉到了信号后,不执行信号的默认处理动作,而是执行回调函数的处理动作
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
参数: 
	- signum -> 要捕捉的信号
 	- handler-> 函数指针, 这是个回调函数
	typedef void (*sighandler_t)(int);// 回调函数的函数原型
		参数:int
		返回值:void
// 实例程序
// 告诉操作系统, 当当前进程收到一个信号SIGINT, 操作系统调用回调函数callback
signal(SIGINT, callback);

sigaction函数-(posix标准)

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
	- signum: 要捕捉的信号的值
	- act: 捕捉信号时,指定的行为->处理动作
	- oldact: 上次捕捉该信号的处理行为;如果不感兴趣, 指定为NULL
		struct sigaction                  //选3个条件
		{
			void     (*sa_handler)(int);  //回调函数1  //第一个和第二个选择其中一个
			void     (*sa_sigaction)(int, siginfo_t *, void *);//回调函数2
			sigset_t   sa_mask;
			int        sa_flags;
			void     (*sa_restorer)(void);// 被废弃了
		};
		sa_mask: 信号集类型
			- 在捕捉到信号,调用处理函数执行的过程中, 临时屏蔽某些信号, 处理函数执行完成, 自动解除屏蔽
			- 不屏蔽, 该变量初始化为0就可以了 -> sigemptyset
			- 假设捕捉的信号是SIGINT, 处理函数在执行过程中, 会自动屏蔽一个信号: SIGINT
		sa_flags:
			- 0, 使用的回调函数是      : sa_handler  , 捕捉的信号不携带数据 -> 常用!!!!!
			- SA_INFO, 使用的回调函数是: sa_sigaction, 捕捉的信号携带数据(一个整型数)

3.2 信号传参(自行学习)

// 发信号不传出函数
kill(pid, signal);

// 将信号发送到某个进程, 并传递一些额外的数据
union sigval
{
    int sival_int;   // 两个进程间
    void *sival_ptr; // 当前进程自己给自己发信号传递数据
};
int sigqueue(pid_t pid, int sig, const union sigval value);

// 信号捕捉 -> 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);                      // 被废弃了
};
// 进程A发送信号并传参 -> 发送信号给进程B
int main(int argc, char *argv[])
{
    if (argc >= 2)
    {
        pid_t pid, pid_self;
        union sigval tmp;

        pid = atoi(argv[1]); // 进程B的PID
                             // 指定通过信号传递的数据
        if (argc >= 3)
        {
            tmp.sival_int = atoi(argv[2]); // 命令行传参,指定传递的参数值
        }
        else
        {
            tmp.sival_int = 100; // 直接指定传递的参数值
        }

        // 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去
        sigqueue(pid, SIGINT, tmp);

        pid_self = getpid(); // 进程号
        printf("pid = %d, pid_self = %d\n", pid, pid_self);
    }

    return 0;
}
// 进程B捕捉进程A发送的信号和参数
// 信号处理回调函数
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
    printf("signum = %d\n", signum);                            // 信号编号
    printf("info->si_pid = %d\n", info->si_pid);                // 对方的进程号
    printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}

int main()
{
    struct sigaction act, oact;

    act.sa_sigaction = signal_handler; // 指定信号处理回调函数
    sigemptyset(&act.sa_mask);         // 阻塞集为空
    act.sa_flags = SA_SIGINFO;         // 指定使用有三个参数的回调函数

    // 注册信号 SIGINT
    sigaction(SIGINT, &act, &oact);

    while (1)
    {
        printf("pid is %d\n", getpid()); // 进程号
        sleep(1);
    }

    return 0;
}

3.3 内核实现信号捕捉的过程

在这里插入图片描述

void callback(int)
{
    ....
    ....;
    .....;
}

int main()
{
    signal(sigint, callback);
    ......
    ......  -> ctrl+c
    .....
     
}

4. 信号集

4.1 阻塞信号集/未决信号集

4.1.1.基本概念

在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改

信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。

信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了 防止信号打断敏感的操作。

记录方式:
	- 通过整数来存储信号状态
	- 整数: 64位整数, 每个标志位对应一个固定的信号
		- 0号标志位 -> 1号信号
		- 1....... -> 2号信号
		..........
     - 标志位取值范围: 0, 1
         
阻塞信号集: 设置某些信号产生之后不能被发送到对应的进程, 因此进程就无法处理这个信号
	- 标志位0: 当前信号没有被阻塞
	- .....1: 当前信号被阻塞
	
未决信号集: 记录了当前进程中已经产生但是没有被处理的信号
	- 标志位0: 当前信号已经递达(被处理了)
	- .....1: 当前信号还没有被处理(未决状态)

4.1.2.两者的关系

在这里插入图片描述
在这里插入图片描述

1. 默认阻塞信号集中不阻塞任何信号, 因此信号产生之后马上就被处理了
2. 我们设置阻塞信号集2号信号阻塞, 该信号对应的标志位 0->1
3. 运行程序, 按ctrl+c, 2号信号SIGINT
4. 在处理这个信号之前, 内核需要先读阻塞信号集, 发现2号信号被阻塞, 因此该信号不能被处理
5. 在未决信号集中2号信号对应的标志位 0 -> 1
6. 在某些条件成熟之后, 阻塞信号集中的2号信号被解除阻塞了, 标志位 1 -> 0
7. 该信号解除阻塞, 内核开始未决信号集中没有被处理的信号, 处理完毕 标志位 1 -> 0

4.2 信号集相关函数

//整体理念:
//	1、调用函数,设置好自定义信号集
//	2、调用函数,把自定义信号集里的数据,设置到阻塞信号集当中	
//	注意:自定义信号集和阻塞信号集类型是相同的,都是一个64位的整型数
1、对自定义信号集进行处理
#include <signal.h>

int sigemptyset(sigset_t *set);
	// 信号集所有的标志位设置为0

int sigfillset(sigset_t *set);
	// 自定义信号集所有的标志位设置为1

int sigaddset(sigset_t *set, int signum);
	// 将自定义信号集中某个信号对应的标志位设置为1

	参数:
		- set: 自定义信号集
		- signum: 信号的编号
            
            
int sigdelset(sigset_t *set, int signum);
	// 将自定义信号集中某个信号对应的标志位设置为0

int sigismember(const sigset_t *set, int signum);
	// 判断自定义信号集中某个信号对应的标志位是0还是1

	返回值: 
		已经设置了: 1
    	没有设置:   0
    	调用失败:  -1
2、将自定义信号集设置到内核的阻塞信号集当中
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
	- how: 
		SIG_BLOCK  : 在内核阻塞信号集原基础上进行追加, 追加要阻塞的信号
		SIG_UNBLOCK: 取消某些信号的阻塞
		SIG_SETMASK: 覆盖内核中的阻塞信号集数据
	- set   : 用户自定义的信号集
	- oldset: 获取内核中的还没有设置之前的数据, 传出参数, 如果不需要这些数据, 指定为NULL即可
3、获取当前内核中未决信号集中的数据信息
#include <signal.h>
int sigpending(sigset_t *set);
参数为传出参数, 函数调用成功之后, 参数的值就会被修改
	在进程中设置某些信号阻塞
		- ctrl+c -> 2号 SIGINT
		- CTRL+\ -> 3号 SIGQUIT
		- 9sigkill(9(无条件杀死)19(无条件暂停)不能被捕捉、阻塞、忽略)
	读未决信号集

在这里插入图片描述

信号集操作总结

在这里插入图片描述

5. SIGCHLD信号

5.1 SIGCHLD的产生条件

1. 子进程死了, 自杀, 他杀
2. 子进程被暂停了
3. 子进程由暂停状态重新恢复运行
以上三种情况子进程都会给父进程发送信号: SIGCHLD, 父进程默认会忽略这个信号
# 在父进程中拦截该信号 -> 写处理函数( 回收子进程资源 )

5.2 使用SIGCHLD回收子进程

// 父子进程之间
// 阻塞信号集和信号的处理动作(sigaction 的回调函数) -> 通过fork 拷贝给子进程
// 							未决信号集是不能拷贝给子进程的
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

// 回调函数, 信号被捕捉之后的处理动作
void recyleChild(int num)
{
    while (1)
    {
        // 回收子进程的资源
        pid_t pid = waitpid(-1, NULL, WNOHANG);
        if (pid > 0)
        {
            printf("child die, pid = %d\n", pid);
        }
        else if (pid == 0)
        {
            printf("child is running...\n");
            break;
        }
        else if (pid == -1)
        {
            printf("all child die\n");
            break;
        }
    }
}

int main()
{
    pid_t pid;
    // 设置信号阻塞
    sigset_t myset;
    sigaddset(&myset, SIGCHLD);
    sigprocmask(SIG_BLOCK, &myset, NULL);

    // 1. 创建子进程, 5ge
    for (int i = 0; i < 50; ++i)
    {
        pid = fork();
        if (pid == 0)
        {
            break;
        }
    }

    if (pid == 0)
    {
        // 子进程
        printf("child process, pid = %d\n", getpid());
    }
    else if (pid > 0)
    {
        // 注册信号捕捉
        struct sigaction act;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        act.sa_handler = recyleChild;
        sigaction(SIGCHLD, &act, NULL);
        // 解除阻塞
        sigprocmask(SIG_UNBLOCK, &myset, NULL);

        // 父进程
        while (1)
        {
            sleep(1);
            printf("parent process, pid = %d\n", getpid());
        }
    }
    return 0;
}

思考问题

// setitimer函数:可以周期性定时
// 第一个参数的三种选择,各自发出的一种信号,但都会杀死进程
//因为发出的信号默认杀死进程,所以我们也就看不到周期性的现象了
//因此,我们需要捕捉信号

--> 定时器函数,一般和信号捕捉函数配合使用

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
参数:
- which: 定时方式
	- ITIMER_REAL: 真实的使用时间(用户区时间 + 内核区时间 + 消耗的时间) -> 最常用的
		- 计时时间到达发送--定时器信号: SIGALRM
    - ITIMER_VIRTUAL: 只计算程序用户区使用的时间
        - 计时时间到达发送--定时器信号: SIGVTALRM
    - ITIMER_PROF: 只计算内核使用的时间
        - 计时时间到达发送--定时器信号: SIGPROF
- new_value: 要设置的定时的周期
    struct itimerval 
	{
		struct timeval it_interval; /* 定时器发送信号的周期 *//* next value */
		struct timeval it_value;    /* 第一次发出信号的时长 *//* current value */
        // 小明从今晚12点睡觉, 设置闹钟, 明天早晨8点起床, 倒计时8小时 -> 闹钟第一次响的时间
		// 小明因为起不来, 又设置了当闹钟第一个响了之后, 每隔5分钟响一次
	};
	// 表示一个时间总和: 秒+微妙
	struct timeval 
	{
		time_t      tv_sec;         /* 秒 *//* seconds */
		suseconds_t tv_usec;        /* 微妙 *//* microseconds */
	};
- old_value: 上一次设置的定时周期, 如果没有需要, 设置为NULL
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值