linux内核学习11:信号

信号 (signal)

signal机制可以被理解成进程的软中断, 用信号处理来模拟操作系统的中断功能

软中断是执行中断指令产生的,而硬中断是由外设引发的。https://zhuanlan.zhihu.com/p/85597791
信号全称为软中断信号,也有人称软中断。

软中断信号(signal,又简称为信号)用来通知进程发生了异常事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

  1. 用户输入命令,在Shell下启动一个前台进程。
  2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
  3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用
    户态 切换到内核态处理硬件中断。
  4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了
    一 个SIGINT信号给该进程)。
  5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,需先处理PCB中记
    录的信号,发现有一个SIGINT信号待处理,用这个信号的默认处理动作是终止进程,所
    以直接终止进程而不再返回它的用户空间代码执行。

linux内核学习11:信号_第1张图片
signal的执行点可以理解成从内核态返回用户态时,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。

Signal机制实现的比较灵活,用户进程由于中断或者system call陷入内核之后,将断点信息都保存到了堆栈中,在内核返回用户态时,如果存在被触发的signal,那么直接将待执行的signal处理函数push到堆栈中,在CPU切换到用户模式之后,直接pop堆栈就可以执行signal处理函数并且返回到用户进程了。Signal处理函数应用了进程上下文,并且应用实际的中断模拟了进程的软中断过程。

如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的实际是:一个进程在即将从内核态返回到用户态时,或者在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程收到的信号实际是在一个进程从内核态返回用户态时,所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理

内 核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。

参考:https://blog.csdn.net/Thanksgining/article/details/41824475?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

1.1 信号列表

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

.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 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

通过指令 kill 信号序号 进程号 可以向一个进程发送信号
其中,举例几个常见的信号:
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGABRT
调用abort函数生成的信号。
SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
kill -9 pid 、kill -SIGKILL
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
kill pid、kill -15 pid 、kill -SIGTERM
SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
SIGSYS
非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

1.2 信号的发送

3.2.1 信号来自内核, 生成信号的请求来自以下3个地方

(1)用户
用户可以通过输入Ctrl-C, Ctrl-\等命令,或是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号。
(2)内核 当进程执行出错时, 内核给进程发送一个信号。
例如,非法段存取,浮点数溢出,亦或是一个非法指令,内核也利用信号通知进程特定事件发生。
(3)进程
一个进程可以通过系统调用kill给另外一个进程发送信号, 一个进程可以和另一个进程通过信号通信。

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

  • kill,将信号发送给进程或者进程组。
  • raise, raise函数向自身所在进程发送一个信号。
  • sigqueue,针对实时信号提出的(当然也支持非实时信号)信号发送函数,通常与函数sigaction配合使用, sigqueue比kill传递了更多的附加信息,但sigqueue只能向一个进程发送信号,不能发送信号给一个进程组。
  • alarm和setitimer都是定时信号系统调用,即可以计时n秒后发出的信号
  • abort 此函数将SIGABRT信号发送给调用进程

下面只举kill的用法

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

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

1.3 信号捕获处理

进程能够通过系统调用signal告诉内核, 它要如何处理信号, 进程有3个选择。

#define SIG_DFL ((void (*) (int)) 0) *语句A*
#define SIG_IGN ((void (*) (int)) 1)
#define SIG_ERR ((void (*) (int)) -1)
SIG_ERR(-1):信号处理函数里, 失败返回SIG_ERR。
SIG_DFL(0):默认信号处理程序
SIG_IGN(1):忽略信号的处理程序

(1)接收默认处理(通常是消亡)
例如,SIGINT的默认处理是消亡, 进程并不一定要使用signal接收默认处理,但是进程能够通过以下调用来恢复默认处理。
signal(SIGINT, SIG_DFL);
(2)忽略信号
程序可以通过以下调用来告诉内核, 它需要忽略SIGINT。
signal(SIGINT, SIG_IGN);
(3)信号处理函数
程序能够告诉内核,当程序到来时应该调用哪个函数。
signal(signum, functionname);

linux内核学习11:信号_第2张图片
SIG_ERR:信号处理函数里, 失败返回SIG_ERR。

1.3.1 信号处理例子

(1)默认处理信号

#include
#include
          
int main()
{             
        signal(SIGINT,SIG_DFL); //该进程如果收到信号,则消亡
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}

这里我们使用ctrl +c,给进程发信号,进程收到信号后,由于定义SIG_DFL,所以进程会消亡。
linux内核学习11:信号_第3张图片

(2)忽略信号

#include
#include
          
int main()
{          
        signal(SIGINT,SIG_IGN);
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}

linux内核学习11:信号_第4张图片

3)使用信号处理处理函数

#include
#include
          
int main()
{         
        void f(int);      
        signal(SIGINT,f);
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}   
    
void f(int signum)
{   
        printf("SIGINT\n");
}

linux内核学习11:信号_第5张图片
linux内核学习11:信号_第6张图片

1.4 sigaction函数使用

信号中,signal是初级用法,sigaction是高级用法

我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。咱么先来看看发送的函数吧。

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

struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};//回调函数句柄sa_handler、sa_sigaction只能任选其一

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。

sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum应该就是注册的信号的编号;第二个参数act如果不为空说明需要对该信号有新的配置;第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

sigaction是信号接收函数,与之对应的是信号发生函数sigqueue

#include 
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
};

信号处理

struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;    //如果设置了SA_SIGINFO属性,说明使用的处理函数是sa_sigaction,而不是sa_handler,否则,系统会默认使用 sa_handler 所指向的信号处理函数。
sigaction(SIGIO, &act, NULL);

//sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

举例:

demo.c

#include
#include
#include 

void handler(int signum, siginfo_t * info, void * context){
    if(signum == SIGIO)
        printf("SIGIO   signal: %d\n", signum);
    else if(signum == SIGUSR1)
        printf("SIGUSR1   signal: %d\n", signum);
    else
        printf("error\n");
    
    if(context)
    {
        printf("content: %d\n", info->si_int);
        printf("content: %d\n", info->si_value.sival_int);
    }
}


int main(void){
    struct sigaction act;
    act.sa_sigaction = handler;//信号处理程序,能够接受额外数据和sigqueue配合使用
    act.sa_flags = SA_SIGINFO;//影响信号的行为SA_SIGINFO表示能够接受数据
    
    sigaction(SIGIO, &act, NULL);
    sigaction(SIGUSR1, &act, NULL);
    for(;;)
    {
        sleep(10000);
    }
    return 0;
}

使用这个函数之前,必须要有几个操作需要完成

  • 如果要获取数据,使用 sigaction 函数安装信号处理程序时,制定SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的是 sa_handler 成员,那么将无法获取额外携带的数据。

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。可以使用 value 参数向信号处理程序传递整数值或者指针值。

sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。

但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。

send.c

#include 
#include 
#include
#include 

int main(int argc, char** argv){
    if(4 != argc)
    {
        printf("[Arguments ERROR!]\n");
        printf("\tUsage:\n");
        printf("\t\t%s   \n", argv[0]);
        return -1;
    }
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);

    if(pid > 0 && sig > 0)
    {
        //int sigqueue(pid_t pid, int sig, const union sigval value);
        union sigval val;
        val.sival_int = atoi(argv[3]);
        printf("send: %d\n", atoi(argv[3]));
        sigqueue(pid, sig, val);
    }
    else
    {
        printf("Target_PID or Signal_Number MUST bigger than 0!\n");
    }
    
    return 0;
}

linux内核学习11:信号_第7张图片

参考https://blog.csdn.net/weixin_44933419/article/details/113368763

你可能感兴趣的:(#,linux内核,linux,运维,服务器)