linux7-信号

发布时间 2023-07-20 14:41:45作者: snowa
1.信号的概念

信号 ->电话铃声 是一种抽象的概念 接电话->动作

信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
![信号实现](I:\7 day\信号实现.png)

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

![信号周期](I:\7 day\信号周期.png)

注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。

2.信号的编号

Linux 可使用命令:kill -l("l" 为字母),查看相应的信号。

编号来区分信号

1-31 常规信号 34-64 实时信号

3.信号四要素

1)编号 2)名称 3)事件 4)默认处理动作

man 7 signal 查看信号对应要素

![信号要素查看](I:\7 day\信号要素查看.png)

4.信号的状态

三种

产生 ctrl +c 中断信号

未决信号 没有被处理

递达信号 被处理的信号

5.阻塞信号集 未决信号集

Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集

阻塞信号集

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

未决信号集

信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。

​ 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

6.信号产生函数
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死)

参数:
    pid : 取值有 4 种情况 :
        pid > 0:  将信号传送给进程 ID 为pid的进程。
        pid = 0 :  将信号传送给当前进程所在进程组中的所有进程。
        pid = -1 : 将信号传送给系统内所有的进程。
        pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
    sig : 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。

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

普通用户不能发送信号 kill 9也是无法发送的

案例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <wait.h>
#include <signal.h>
//匿名映射
int main()
{
	//结束子进程
	int ret = -1;
	pid_t pid = -1;
	//创建子进程
	pid = fork();
	if (-1 == pid)
	{
		perror("fork");
		return -1;
	}
	//子进程死循环
	if (0 == pid)
	{
		while (1)
		{
			printf("子进程运行\n");
			sleep(1);
		}
		exit(0);
	}
	else
	{
		sleep(3);
		printf("发送结束子进程信号\n");
		kill(pid, 15);
	}

}
7.向自己发送信号
#include <signal.h>

int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
    sig:信号编号
返回值:
    成功:0
    失败:非0值

案例

int main()
{
int c=0;
		while (1)
		{
			printf("进程运行\n");
			sleep(1);
c++;
if(c>5)
{
printf("发送结束子进程信号\n");
raise(15);
}
		}
	sleep(800);
}
8.给自己发送异常信号
#include <stdlib.h>

void abort(void);
功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);

参数:无

返回值:无
9.设置超时闹钟
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
功能:
    设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
    取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
    seconds:指定的时间,以秒为单位
返回值:
    返回0或剩余的秒数

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

测试程序:

//写fifo1 读fifo2
int main(void)
{
    unsigned int ret = 0;
    //5秒后超时

    ret = alarm(500);

    printf("%u \n", ret);
    sleep(2);
    printf("过 了 2秒 %u \n", ret);
    ret = alarm(3);
    printf(" 新定 闹钟后 的%u \n", ret);

    getchar();
    return 0;
}

程序还是会在3秒内结束 闹钟的覆盖下

10.定时器2
#include <sys/time.h>
int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
功能:
    设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
    which:指定定时方式
        a) 自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
        b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM  只计算进程占用cpu的时间
        c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
    new_value:struct itimerval, 负责设定timeout时间
        struct itimerval {
            struct timerval it_interval; // 闹钟触发周期
           struct timerval it_value;    // 闹钟触发时间
        };
        struct timeval {
            long tv_sec;            // 秒
            long tv_usec;           // 微秒
        }
        itimerval.it_value: 设定第一次执行function所延迟的秒数 
        itimerval.it_interval:  设定以后每几秒执行function
    old_value: 存放旧的timeout值,一般指定为NULL
返回值:
    成功:0
    失败:-1

案例

int main(void)
{
   
	int ret = -1;
	struct itimerval tmo;
	//第一次触发时间
	tmo.it_value.tv_sec = 3;
	tmo.it_value.tv_usec = 0;
	//触发周期
	tmo.it_interval.tv_sec = 2;
	tmo.it_interval.tv_usec = 0;
	//3秒后触发定时器 之后每2秒执行一次周期

	ret = setitimer(ITIMER_REAL, &tmo, NULL);
	while (1);

}
11.定时器于信号捕捉的配合

信号处理 ->信号捕捉
signal函数

#include <signal.h>

typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);///回调函数 函数指针
功能:
    注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。

参数:
    signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。
    handler : 取值有 3 种情况:
          SIG_IGN:忽略该信号
          SIG_DFL:执行系统默认动作
          信号处理函数名:自定义信号处理函数,如:func
          回调函数的定义如下:
            void func(int signo)
            {
                // signo 为触发的信号,为 signal() 第一个参数的值
            }

返回值:
    成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
    失败:返回 SIG_ERR

该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。

案例

void fun2(int signum)
{
	printf("捕捉到 %d\n", signum);
}
//写fifo1 读fifo2
int main(void)
{
   
		int ret2 = -1;
	//ctrl c
	signal(SIGINT, fun2);
	//ctrl \;
	signal(SIGQUIT, fun2);

	signal(SIGALRM, fun2);
	int ret = -1;
	struct itimerval tmo;
	//第一次触发时间
	tmo.it_value.tv_sec = 3;
	tmo.it_value.tv_usec = 0;
	//触发周期
	tmo.it_interval.tv_sec = 2;
	tmo.it_interval.tv_usec = 0;
	//3秒后触发定时器 之后每2秒执行一次周期

	ret = setitimer(ITIMER_REAL, &tmo, NULL);//触发14的信号
	while (1);
}

12.信号集

在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。

​ 这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。

![信号集](I:\7 day\信号集.png)

13.自定义信号集函数

个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集(信号的集合)。

​ 信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。

相关函数说明如下:

#include <signal.h>  

int sigemptyset(sigset_t *set);       //将set集合置空
int sigfillset(sigset_t *set);          //将所有信号加入set集合
int sigaddset(sigset_t *set, int signo);  //将signo信号加入到set集合
int sigdelset(sigset_t *set, int signo);   //从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); //判断信号是否存在

除sigismember外,其余操作函数中的set均为传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

案例代码

void show_set(sigset_t* s)
{
	int i = 0;
	for (i = 1; i < 32; i++)
	{
		if (sigismember(s,i))
		{
			printf("1");
		}
		else
		{
			printf("0");
		}


	}

}

//写fifo1 读fifo2
int main(void)
{
	//信号注册
	int i = 0;
	sigset_t set;
	//清空集合
	sigemptyset(&set);
	show_set(&set);
	//加入信号
	sigfillset(&set);
	show_set(&set);
	//将信号2 和信号3 移除
	sigdelset(&set, SIGINT);
	show_set(&set);

}

14.信号阻塞集

信号阻塞集暂缓信号的传递 如果阻塞信号从阻塞集删除 那么进程就会收到信号

每个进程都有信号屏蔽集合

我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。

参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
    set : 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
    oldset : 保存原先信号阻塞集地址

返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 ENVAL,表示参数 how 不合法。

实例

void func1(int xh)
{

	printf("捕捉到信号\n");
}
int main(void)
{
	sigset_t set;
    sigset_t oldset;

	//注册信号 ctrl c

	signal(SIGINT, func1);
	printf("按下任意符号阻塞信号2");
	getchar();
	sigemptyset(&set);
	sigemptyset(&oldset);
	//将阻塞信号加入信号集
	sigaddset(&set, SIGINT);
	//屏蔽
	sigprocmask(SIG_BLOCK, &set, &oldset);
	printf("按下任意符号解除阻塞信号2");
	sigprocmask(SIG_BLOCK, &oldset, &set);
	getchar();
}
15 .未决信号集合

内核控制 只能读 不能写 可以通过函数sigismember判断

sigpending函数

#include <signal.h>

int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
    set:未决信号集
返回值:
    成功:0
    失败:-1

16.捕捉信号

进程收到信号处理方法 1.执行系统默认工作 2.忽略 3.执行自定义函数(捕获)

【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

![信号捕捉](I:\7 day\信号捕捉.png)

signal函数

17.sigaction函数
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。

参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。

    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。

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

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); //已弃用
};
void fun(int xh, siginfo_t*info,void* contex)
{

	printf("捕捉到信号\n");
}
int main(void)
{
	//创建结构体
	struct sigaction act;
	struct sigaction oldact;
	//结构体赋值
	act.sa_sigaction = fun;
	act.sa_flags = SA_SIGINFO;
	//信号注册
	sigaction(SIGINT, &act, NULL);
getchar();


}

18.发送信号(了解)

向指定进程发送信号

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
功能:
    给指定进程发送信号。
参数:
    pid : 进程号。
    sig : 信号的编号。
    value : 通过信号传递的参数。
        union sigval 类型如下:
            union sigval
            {
                int   sival_int;
                void *sival_ptr;
            };
返回值:
    成功:0
    失败:-1

向指定进程发送指定信号的同时,携带数据。但如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。 

​ 下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。

19.不可重入 可重入函数

函数永久修改其他区域数据 不可重入

满足下列条件的函数多数是不可重入(不安全)的:

  • 函数体内使用了静态的数据结构;
  • 函数体内调用了malloc() 或者 free() 函数(谨慎使用堆);
  • 函数体内调用了标准 I/O 函数。

可重入函数

  • 在写函数时候尽量使用局部变量(例如寄存器、栈中的变量);
  • 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。
20.防止僵尸进程

SIGCHLD信号产生的条件

​ 1) 子进程终止时

​ 2) 子进程接收到SIGSTOP信号停止时

​ 3) 子进程处在停止态,接受到SIGCONT后唤醒时

如何避免僵尸进程

  1. 最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
  2. 如果父进程要处理的事情很多,不能够挂起,通过 signal() 函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。

示例程序:

void sig_child(int signo)
{
    pid_t  pid;

    //处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("child %d terminated.\n", pid);
    }
}

int main()
{
    pid_t pid;

    // 创建捕捉子进程退出信号
    // 只要子进程退出,触发SIGCHLD,自动调用sig_child()
    signal(SIGCHLD, sig_child);

    pid = fork();   // 创建进程
    if (pid < 0)
    { // 出错
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子进程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);
    }
    else if (pid > 0)
    { // 父进程
        sleep(2);   // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    }

    return 0;
}

如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。

int main()
{
    pid_t pid;

    // 忽略子进程退出信号的信号
    // 那么子进程结束后,内核会回收, 并不再给父进程发送信号
    signal(SIGCHLD, SIG_IGN);

    pid = fork();   // 创建进程

    if (pid < 0)
    { // 出错
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子进程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);

    }
    else if (pid > 0)
    { // 父进程
        sleep(2);   // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    }

    return 0;
}