[Linux]信号量机制

时间:2021-09-07 15:13:52

1.信号简介
每一个信号都对应一个正整数常量(称为signal number,即信号编号。定义在系统头文件signal.h中),代表同意用户的诸多进程之间的传送事先约定的信号类型,用于通知某进程发生了某异常事件。每个进程在运行的时候,都要通过信号机制来检查是够有信号到达,若有信号到达,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。实质上,信号机制是对中断机制的一种模拟,故在早期的UNIX版本中又把它称为软中断。
⑴ 信号与中断的相似点:
①采用了相同的异步通信方式;
②当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
③都在处理完毕后返回到原来的断点;
④对信号或中断都可进行屏蔽。
⑵ 信号与中断的区别:
①中断有优先级,而信号没有优先级,所有的信号都是平等的;
②信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
③中断响应是及时的,而信号响应通常都有较大的时间延迟。
⑶ 信号机制具有以下三方面的功能:
①发送信号。发送信号的程序用系统调用kill( )实现;
②预置对信号的处理方式。接收信号的程序用signal( )来实现对处理方式的预置;
③收受信号的进程按事先的规定完成对相应事件的处理。

2.信号从哪里来
信号来自内核,生成信号的请求来自3个地方:
(1)用户
用户能够通过输入CTRL+c、Ctrl+,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
(2)内核
当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、浮点数溢出等;内核也利用信号通知特定事件的发生。
(3) 进程
一个进程可以通过系统调用kill给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。

由进程中的某个操作产生的信号称为同步信号,例如被零除。由像用户击键这样的进程之外的事件引起的信号称为异步信号。
3.信号列表
[Linux]信号量机制
4.进程如何处理信号
当进程接收到SIGINT时,并不一定要消亡。进程能够通过系统调用signal告诉内核它要如何 处理信号。进程有三个选择:
(1)接受默认处理(通常是消亡)
手册上列出了对每个信号的默认处理SIGINT的默认处理是消亡。进程不一定要使用signal接受默认处理,但是进程可以通过以下调用来恢复默认处理:

signal(SIGINT,SIG_DFL);

(2)忽略信号
程序可以通过以下调用告诉内核它需要忽略SIGINT信号:

signal(SIGINT,SIG_IGN);

(3)调用一个函数
这种选择是最强大的一种。程序能告诉内核,当信号到来的时候应该调用哪一个函数。当信号到来的时候调用的函数叫做信号处理函数

//functionname是函数名
signal(SIGINT,functionname);

看例子

//create by Gpwner 2017年1月6日10:09:51
#include<stdlib.h>
# include<stdio.h>
# include<signal.h>
# include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int wait_mark;
void waiting(),stop();
void main()
{
    int  p1, p2;
    signal(SIGINT,stop);
    while((p1=fork())==-1);
    if(p1>0)/*在父进程中*/
    {
        while((p2=fork())==-1);
        if(p2>0)/*在父进程中*/
        {
            wait_mark=1;
            waiting(0);
            kill(p1,10);
            kill(p2,12);
            wait(NULL);
            wait(NULL);
            printf("parent process is killed!\n");
            exit(0);
        }
        else/*在子进程2中*/
        {
            wait_mark=1;
            signal(12,stop);
            waiting();
            lockf(1,1,0);
            printf("child process 2 is killed by parent!\n");
            lockf(1,0,0);
            exit(0);
        }
    }
    else/*在子进程1中*/
    {
        wait_mark=1;
        signal(10,stop);
        waiting();
        lockf(1,1,0);
        printf("child process 1 is killed by parent!\n");
        lockf(1,0,0);
        exit(0);
    }
}
void waiting()
{
    while(wait_mark!=0);
}
void stop()
{
    wait_mark=0;
}

这个例子是通过父进程分别创建了两个子进程,然后让父进程捕捉键盘上来的中断信号(即按ctrl+c键),当捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号(向子进程p1发送信号10,向子进程p2发送信号12),子进程捕捉到父进程发来的信号

看运行结果:
[Linux]信号量机制
5.kill函数

int  kill(pid,sig)

参数定义

int pid,sig;
其中,pid是一个或一组进程的标识符,参数sig是要发送的软中断信号。
(1)pid>0时,核心将信号发送给进程pid。
(2)pid=0时,核心将信号发送给与发送进程同组的所有进程。
(3)pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。
6.lockf函数
lockf()函数允许将文件区域用作信号量(监视锁),或用于控制对锁定进程的访问(强制模式记录锁定)。试图访问已锁定资源的其他进程将返回错误或进入休眠状态,直到资源解除锁定为止。当关闭文件时,将释放进程的所有锁定,即使进程仍然有打开的文件。当进程终止时,将释放进程保留的所有锁定。

参数说明:

#include <unistd.h>
int lockf(int fd, int cmd, off_t len);

①fd 是打开文件的文件描述符。
为通过此函数调用建立锁定,文件描述符必须使用只写权限(O_WRONLY)或读写权限(O_RDWR)打开。如果调用进程是具有PRIV_LOCKRDONLY 权限的组的成员,它也可以使用lockf()来锁定使用只读权限(O_RDONLY)打开的文件。

②cmd 是指定要采取的操作的控制值,允许的值在中定义。
如下所示:

# define F_ULOCK 0 //解锁
# define F_LOCK 1 //互斥锁定区域
# define F_TLOCK 2 //测试互斥锁定区域
# define F_TEST 3 //测试区域

F_ULOCK 请求可以完全或部分释放由进程控制的一个或多个锁定区域。如果区域未完全释放,剩余的区域仍将被进程锁定。如果该表已满,将会返回[EDEADLK]错误,并且不会释放请求的区域。
使用 F_LOCK 或 F_TLOCK 锁定的区域可以完全或部分包含同一个进程以前锁定的区域,或被同一个进程以前锁定的区域包含。此时,这些区域将会合并为一个区域。如果请求要求将新元素添加到活动锁定表中,但该表已满,则会返回一个错误,并且不会锁定新区域。
F_LOCK 和 F_TLOCK 请求仅在采取的操作上有所差异(如果资源不可用)。如果区域已被其他进程锁定,F_LOCK 将使调用进程进入休眠状态,直到该资源可用,而 F_TLOCK 则会返回[EACCES]错误。
F_TEST 用于检测在指定的区域中是否存在其他进程的锁定。如果该区域被锁定,lockf()将返回 -1,否则返回0;在这种情况下,errno 设置为[EACCES]。F_LOCK 和 F_TLOCK 都用于锁定文件的某个区域(如果该区域可用)。F_ULOCK 用于删除文件区域的锁定。

③len是要锁定或解锁的连续字节数。
要锁定的资源从文件中当前偏移量开始,对于正 len 将向前扩展,对于负 len 则向后扩展(直到但不包括当前偏移量的前面的字节数)。如果 len 为零,则锁定从当前偏移量到文件结尾的区域(即从当前偏移量到现有或任何将来的文件结束标志)。要锁定一个区域,不需要将该区域分配到文件中,因为这样的锁定可以在文件结束标志之后存在。
使用 S_ENFMT 文件模式的常规文件(未设置组执行位)将启用强制策略。启用强制策略后,如果清除了 O_NDELAY,访问锁定区域的读取和写入将进入休眠状态,直到整个区域可用为止,但是如果设置了O_NDELAY,将会返回−1并设置 errno。由其他系统函数(如 exec())访问的文件不受强制策略的影响。

再来看这个例子:

#include<stdlib.h>
# include<stdio.h>
# include<signal.h>
# include<unistd.h>
int  pid1, pid2;
int  EndFlag=0;
int pf1=0;
int pf2=0;
void IntDelete()
{
    kill(pid1,10);
    kill(pid2,12);
    EndFlag=1;
}
void Int1()
{
    printf("child process 1 is killed by parent !\n");
    exit(0);
}
void Int2()
{
    printf("child process 2 is killed by parent !\n");
    exit(0);
}
main()
{
    int exitcode;
    signal(SIGINT,SIG_IGN);
    while((pid1=fork())==-1);
    if(pid1==0)
    { 
        signal(SIGUSR1,Int1);
        signal(SIGINT,SIG_IGN);
        pause();
        exit(0);
    }
    else
    {
        while((pid2=fork())==-1);
        if(pid2==0)
        {
            signal(SIGUSR2,Int2);
            signal(SIGINT,SIG_IGN);
            pause();
            exit(0);
        }
        else
        {

            waitpid(-1,&exitcode,0);        /*等待任何子进程中断或结束*/
            printf("parent process is killed \n");
            exit(0);
        } 
    }
}

此时编译之后不管给它多少次终端信号都将被忽略
[Linux]信号量机制

只要我们将main函数中这句话去掉

signal(SIGINT,SIG_IGN);

然后再父进程中加上这句就可以正常中断了:

 signal(SIGINT,IntDelete);