linux学习——信号

信号

标签(空格分隔): 未分类


今天我们来说一说信号,linux当中有一个头文件signal.h其中提供了62个信号。信号是用于向一个进程来通知发生一部时间的机制。信号类似于一个硬件终端,但是信号没有优先级,操作系统看待信号都是平等的。对一个进程,一次只能给一个信号。

所以信号我们也叫做软中断信号,通知进程发生异步时间,使用信号我们可以通过

上述的信号,1-31我们叫做普通信号,34-64我们叫做实时信号,实时信号主要用于实时系统。

产生信号的条件:


  1. 用户在终端按下键盘的某些键的时候。例如:Ctrl+C终止进程。Ctrl+\ 停止进程,引发进程停止并且产生信息转储。Ctrl+Z停止进程执行,只是暂停执行,不能被阻塞,处理或忽略
  2. 硬件产生信号,例如:除数为0,无效的内存引用,通常是由硬件检测到传给内核,然后内核通知进程。
  3. 通过系统调用kill将信号传送给另外的进程。

  4. 可以在终端下使用kill命令将信号发送给其他进程。

  5. 一些软件条件发生的时候,也可能产生信号,例如:在网络连接上传来外来数据的时候产生SIGURG。

信号的处理方式


对于进程来说,不能判别是否出现一个信号,而是必须要告诉内核信号出现的时候,执行下列操作。
信号的处理方式有三种:
1. 忽略此信号
2. 执行信号的默认处理动作。
3. 提供自定义行为,要求处理该信号的时候切换到用户态执行这个处理函数,也叫做捕捉一信号。

注:捕捉信号的时候需要注意不能捕捉SIGKILL信号和SIGSTOP信号。当捕捉到SIGCHLD信号,这个时候标识一个子进程已经终止,所以这个时候我们可以调用waitpid函数来取得该子进程的进程ID以及它的终止状态。

产生信号


1. 终端产生信号
首先提出一个概念叫做 core dump,我想在linux下写c,肯定不少发现错误的时候报这个错误接下来我们先来看看这个东西到底是个什么。

core dump叫做核心转储,也叫做核心文件(core file),是操作系统在进程收到某些信号而终止运行时,将此时进程的地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件,这个信息我们常常用于调试程序。

默认的linux系统当中是不生成这个文件的,我们可以使用ulimit -a查看系统中这个文件的大小。
![enter description here][1]
我们可以使用命令ulimit -c xxxx设置生成的core dump的大小。
![enter description here][2]
默认情况下,生成的core dump文件的格式是core.xxx,后面一般都是pid。并且生成在当前目录下。

现在我们模拟生成一下这样的core dump文件,我们首先写出一个死循环。

int main()
{
    printf("hello world\n");
    while(1);

    return 0;
}

我们运行这个程序,然后操作,Ctrl+\,这样就会出现:
![enter description here][3]
从上图我们可以看到我们操作过程中从键盘Ctrl+,这样就会产生一个信号SIGQUIT,这个信号传递给运行的进程,然后进程得到这个信号引发终止进程并引发核心转储。

接下来我们来看看如何利用这个coredump文件进行调试
我们直接gdb test文件和core文件就好8,在终端输入gdb test core.6437得到:
![enter description here][4]

是不是很快就定位到了错误之处!

2. 通过系统调用产生信号。
我们可以通过系统调用来产生信号。这里我们先来看一下kill函数,

 int kill(pid_t pid, int sig);

kill函数可以给指定的进程发送信号。
这个函数当中,第一个参数是进程的pid,第二个参数是我们需要发送给pid进程的一个信号的序号,比如我们传sig为9,那我们就发送信号SIGKILL。

接下来需要介绍的一个函数叫做raise函数 :

int raise(int sig);

这个函数是用来给当前进程发送信号的。

abort函数使得当前进程接收到信号而异常中止。

void abort(void);

这个函数会产生SIGABRT信号,这个信号是夭折信号。

3.软件产生信号

软件产生信号这里我们首先来说一个函数alarm函数:

unsigned int alarm(unsigned int seconds);

里面的变量seconds所给的是一个时间,单位是秒。这个函数的意思就是类似闹钟的形式,alarm(1)的意思让操作系统在1秒钟以后结束这个进程alarm的默认行为动作就是终止这个进程。alarm函数的信号SIGALRM信号,这个信号的默认动作就是终止这个进程,当使用alarm(0)的意思就是取消以前设定的闹钟,返回值就是所剩余的时间。调用alarm函数会产生SIGALRM信号。

阻塞信号


1.关于阻塞概念
阻塞信号我们首先提出一些概念,

信号递达:正在执行信号处理的动作,

信号产生与信号递达之间叫做信号未决,也叫做pending。

当信号阻塞的时候不会递达,接触阻塞,信号才能递达。

关于信号,我们首先需要从内核的角度来看看信号。
在内核当中,当一个进程接收到信号,会对应的在进程的PCB当中有三个相关的结构,

![enter description here][5]

因为我们现在有31个普通信号,所以这个时候我们可以想下我们前期所说的位图,我们也就可以利用一个整形就够了,每一个信号对应一个比特位。

另外因为是bit位,所以这里注意,即使你产生了多个信号,这里的信号位也只是从0变为1,不记录信号产生了多少次。

pending表标识信号未决表,表示信号是否产生,block阻塞表,表示当前进程与信号屏蔽相关内容。我们也把阻塞信号集叫做当前进程的信号屏蔽字。

注意阻塞和忽略是两回事,阻塞只是屏蔽了信号,而忽略是对信号的一种处理方式。

2.信号集相关的函数
在linux下信号我们定义成为sigset_t类型的,sigset_t我们叫做信号集,这种类型经过我的测试大小是128个字节。
信号集下面有一些函数。

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); 

这里的函数都放在signal.h当中,sigemptyset函数用来初始化set所指向的信号集,使得信号集所有信号的对应的bit位清空。
sigfillset函数标识对set所指向的信号集的所有位进行置位操作。
注意,使用信号集之前一定得先试用sigemptyset或者是sigfillset进行初始化信号集。
sigaddset是对set所指向的信号集进行进行添加一个信号signo。
sigdelset函数是对信号集进行删除有效的信号。
sigismember函数是用来判断是否在set所指向的信号集当中包含signo信号。

说完看这些函数我们再说一个和信号屏蔽字相关的函数,sigprocmask函数,

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

这个函数是用来进行读取或者修改进程的信号屏蔽字这里的how说的是如何进行更改,set指向你要修改的当前信号屏蔽字,oldset指向修改前你的信号屏蔽字。
how参数:

参数 含义
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中移除阻塞的信号
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值

注意:如果调用了sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少会将其中的一个信号递达。

接下来说另外的一个函数叫做sigpending,它用来输出pending表中的内容。

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void printfspending(sigset_t *set)
{
    int i=0;
    for(i=0;i<32;i++)
    {
        if(sigismember(set,i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    printfspending(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,NULL);
    while(1)
    {
        sigpending(&oset);
        printfspending(&oset);
        sleep(1);
    }

    return 0;
}

我们可以从图片当中看到当我们按下Ctrl+c产生SIGINT信号的时候,这个时候就会在未决表改了对应的比特位。SIGINT信号是2号信号,修改了下标为2的位置的比特位。

捕捉信号


接下来我们来着重讲一讲关于信号捕捉的问题。

这里先来提出一个函数就叫做sigaction函数,这个函数可以修改和信号相关联的动作,实现信号的捕捉。

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

struct sigaction的定义:

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);
};
参数 含义
sa_handler 早期的捕捉函数
sa_sigaction 新添加的捕捉函数,通过sa_flags选择哪种捕捉函数。
sa_mask 在执行捕捉函数时,设置阻塞其它信号,sa_mask,进程阻塞信号集,退出捕捉函数后,还原回原有的阻塞信号集
sa_flags SA_SIGINFO 或者 0
sa_restorer 保留,已过时

我们也可以使用signal函数可以实现这个功能。

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

它的第一个参数是信号的编号,第二个参数是指向自定义函数的指针,就是当你捕捉到这个信号,不让它去做它的默认操作,而是去做你想要让它做函数,这个参数是一个返回值为void,参数为int的一个函数指针。

signal是C标准库提供的信号处理函数,

接下来说一说信号捕捉的时候的状态转换:
![enter description here][6]

从上面这张图就可以看出整个状态的转换,

1.首先当你遇到中断、异常或者系统调用的时候进入内核态。
2.然后产生信号,这样由内核态切换用户态,这个过程当中需要去PCB检查那三张表,然后发现有递达的信号,然后这个时候就去处理信号对应的操作。也就是信号处理函数。
3.处理信号处理函数的时候,这个时候为了安全的问题,这个时候为用户态。
4.信号处理函数结束后,然后从用户态切换到内核态。
5.然后由内核态切换到中断异常执行处的用户态。

所以总共有4次状态的切换。

可重入函数


有了信号以后,会去调用喜好处理函数,这个时候你的程序就是异步执行,这个时候就引入了一个问题就是可重入函数的问题,

对于一个函数,当多个执行流进入函数,运行期间会出现问题的就叫做不可重入函数,不会出现的问题就是可重入函数。

信号捕捉函数内部禁止调入不可重入函数。

另外可重入函数还会和线程安全有联系:
1.线程安全不一定是可重入的,可重入的一定是线程安全的。
2.对全局变量或者公共资源进行多线程的进行访问的时候,则这个就既不是线程安全的也不是可重入。
3.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

四类不可重入函数u:
第一类:不保护共享变量的函数
第二类:保持跨越多个调用的状态函数
第三类:返回指向静态变量指针的函数。
第四类:调用线程不安全的函数。

可重入函数是线程安全函数的一种,特点在于它们被多个线程调用的时候,不会引用任何共享数据。

对于不可重入函数的处理,我们通常采用的方法就是重写函数。

另外就是有些以_r结尾的函数就是那个函数的可重入版本。

竞态条件


我们先来介绍一个pause函数。

 int pause(void);

关于pause函数,是用来使得调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数不返回,如果处理动作是忽略,pause函数也不返回。如果处理动作是信号捕捉,则调用捕捉函数,然后返回-1。

然后这里我们使用alarm和pause模拟实现一个sleep函数。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sig_alarm(int signo)
{

}
void mysleep(int seconds)
{
    struct sigaction set,oset;
    set.sa_handler=sig_alarm;
    sigemptyset(&set.sa_mask);
    set.sa_flags=0;
    sigaction(SIGALRM,&set,&oset);
    //设置闹钟
    alarm(seconds);
    //这里闹钟到时间发送信号SIGALRM,然后执行信号处理函数,然后pause返回错误码-1,
    pause();
    unsigned int unslept=alarm(0);
    sigaction(SIGALRM,&oset,NULL);
}
int main()
{
    while(1)
    {
        mysleep(2);
        printf("2 seconds success\n");
    }
    return 0;
}

我们这个函数mysleep模拟了sleep函数。但是,我们需要思考一个问题就是在这里存在一个时序竟态的问题,当我们执行完alarm之后,别的进程会竞争夺走了CPU,夺走n秒后,SIGALRM递达了,然后n秒过后,这个时候就去执行pause,这样没有了信号,这样最终就是一直挂起。

所以我们要让alarm和pause的操作是原子的才行。

linux在这里给出了一个函数sigsuspend函数。

 int sigsuspend(const sigset_t *mask);

1.通过mask来临时解除对某个信号的屏蔽
2.挂起等待
3.然后当sigsuspend返回的时候,这个时候恢复为原来的值

所以我们应该对这一段代码这样操作才行

    //首先屏蔽SIGALRM信号,不让它递达
    alarm(seconds);
    //解除屏蔽字,SIGALRM递达,
    pause();

所以我们先阻塞信号,保存当前信屏蔽字,然后直到最后进程回到我当前进程,然后我解除SIGALRM信号的屏蔽,这样信号就会递达这样就确保了alarm和pause之间的操作都是原子的。

而对于sigsuspend函数来说:sigsuspend用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。

SIGCHLD


最后我们来说一个信号,是SIGCHLD信号,这个信号是我们子进程终止的时候会给父进程传送这个信号。
SIGCHLD信号产生的条件:
1.子进程终止时
2.子进程收到SIGSTOP信号停止的时候。
3.子进程处在停止状态,接受到SIGCONT后唤醒。

父进程接收到了SIGCHLD信号,这个时候的默认动作是忽略,当然你可以去进行信号捕捉。我们能通过信号捕捉可以去处理其他。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章