• 信号(软中断)


    一、概述

    1. 信号是事情发生时对进程的通知机制,也称软中断,信号提供了一种处理异步事件的方法。

    2. 不存在编号为0的信号。

    3. 信号与硬件中断的相似之处在于打断了程序执行的正常流程,有的情况下,无法预测信号到达的精确时间。

    4. 信号因某些事情而产生,信号产生后,会于稍后被传递给某一进程,而进程也会采取某些措施(忽略、杀死、停止进程、执行信号处理器程序等)来响应信号,在产生和到达期间,信号处于的呢过到状态。

    信号类型和行为:
    在这里插入图片描述

    二、函数signal

    //信号机制简单接口
    #include  
    void (*signal(int signo,void (*handler)(int)))(int);
    //signo:如上图所示。
    //handler的值是常量SIG_IGN、常量SIG_DFL或 当接到此信号后要调用的函数的地址。
    
    //signo:整型,第二个参数指函数指针。
    //函数无返回值,且接收一个整型参数。
    //返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR
    
    //简化:
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signo,sighandler_t fun);
    

    例:循环处理

    #include
    static void sigHandler(int sig)
    {
       
    	printf("Ouch!\n");
    }
    int main(int argc,char *argv[])
    {
       
    		int j;
    		if(signal(SIGINT,sigHandler)==SIG_ERR)
    		errExit("signal");
    		for(j=0;;j++)//持续循环。将递增计数器值打印出来,然后休眠几秒
    		{
       
    		printf("%d\n",j);
    		sleep(3);
    		}
    }
    

    三、可重入函数和不可重入函数

    1. 同一个进程的多条线程可以同时安全的调用某一函数,则为可重入的。无论其他线程调用该函数的状态如何,函数均可产生预期结果。可重入被称为异步信号安全的。如下图所示。

    2. 未使用不可重入函数也可能造成,由于每个线程都有一个errno变量,所以信号处理程序可能会修改其原先值。(方法:入口处保存errno值,出口恢复。)void handler(int sig){int savedErro;savedErro=errno; errno = savedErrno;}

    3. 更新全局变量或静态数据结构的函数可能是不可重入的(只用到本地变量的函肯定是可重入的)。两个线程同时更新同一全局变量或数据结构类型,会产生不正确的结果。(a.更改全局变量或数据结构;b.调用malloc或free;c.标准I/O函数)。

    在这里插入图片描述

    //信号处理程序my_alarm调用不可重入函数getpwnam
    //my_alarm每秒钟被调用一次。
    #include
    static void my_alarm(int signo)
    {
       
    	struct passwd *rootptr;
    	pritnf("in signal handler\n");
    	if((root = getpwnam("root")) == NULL)
    			err_sys("getpwnam(root)" error);
    			alarm(1);
    }
    
    int main(void)
    {
       
    	struct passwd *ptr;
    	signal(SIGALRM,my_alarm);
    	alarm(1);
    	for(;;){
       
    		if((ptr = getpwnam("sar"))==NULL)
    			err_sys("getpwnam error");
    			if(strcmp(ptr->pw_name,"sar")!=0)
    				printf("return value corrupted!,pw_nmae=%s\n",ptr->pw_name);
    		}
    }
    

    1.全局变量和sig_atomic_t数据类型

    1. 在主程序和信号处理函数之间共享全局变量。信号处理函数可能会随时修改全局变量,但只要主程序能够正确处理,共享全局变量是安全的。

    2. 例:信号处理函数只设置全局标志。主程序则周期性地检查这标志,采用相应信号传递(同时清除标志)。当信号处理函数以此方式来访问全局变量,应在声明变量时使用volatile关键字,从而防止编译器将其优化到寄存器中。

    3. 对全局变量读写不至一条机器指令,而信号处理函数就可能子啊这些指令序列中之间将主程序中断(非原子操作)。C提供了一种数据类型sig_atomic,确保读写操作的原子性。

    所以所有在主程序与信号处理函数之间共享的全局变量应按如下声明:

    volatile sig_atomic flag;
    //SIG_ATOMIC_MIN和SIG_ATOMIC_MAX规定赋给sig_atomic_t类型的值范围。
    

    四、函数kill和raise

    #include  
    //信号编号0定义为空信号。
    //向并不存在得到进程发送空信号,kill返回-1,error被设置为ESRCH。
    //调用kill为调用进程产生信号,此信号不被阻塞,kill返回之前signo或者某个其他未决定、非阻塞信号被传送到该进程。
    int kill(pid_t pid, int signo); //将信号发送给进程或进程组
    
    
    int killpg(pid_t pgrp,int signo);//向某进程组的所有成员发送一个信号。
    //相对于
    kill(-pgrp,signo);//pgrp为0,向调用者所属进程组的所有信号发送此信号。
    
    
    int raise(int signo); //允许进程向自身发送信号
    //两个函数返回值:若成功,返回0;若出错,返回−1 调用
    
    
    
    //调用
    raise(signo); 
    //等价于调用
    kill(getpid(), signo);
    

    pid参数4中不同的情况:

    1. pid > 0 将该信号发送给进程ID为pid的进程。

    2. pid==0将发送信号给调用与调用进程同组的每个进程,包括调用进程的自身。

    3. pid<-1将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。

    4. pid == −1 将该信号发送给发送进程有权限向它们发送信号的所有进程。如 前所述,所有进程不包括系统进程集中的进程。

    5. 无进程与指定的pid相匹配,则kill调用失败,同时errno置为ESRCH(查无此进程)。

    进程要发送信号给另一个进程,需适当的特权,规则如下:

    1. 超级用户可以将信号发送给任一进程。

    2. root用户和组运行的init进程是种特例,仅能接收已安装了处理函数的信号。防止系统管理员意外杀死init进程。

    3. 非超级用户,规则是发送者的实际ID或有效用户ID必须等于接收者的实际用户ID或有效ID。在这里插入图片描述

    4. 支持_POSIX_SAVED_IDS,检查接收者的保存设置用户ID(而不是有效用户ID)。

    5. 如果被发送的信号是SIGCONT,则进程可将发送给属于同一会话的任一其他进程。

    //使用kill()系统调用
    #include
    
    int main(int argc, char *argv[])
    {
       
        int s, sig;
    
        if (argc != 3 || strcmp(argv[1], "--help") == 0)
            usageErr("%s pid sig-num\n", argv[0]);
    
        sig = getInt(argv[2], 0, "sig-num");
    
        s = kill(getLong(argv[1], 0, "pid"), sig);
    
        if (sig != 0) {
       
            if (s == -1)
                errExit("kill");
    
        } else {
                           /* Null signal: process existence check */
            if (s == 0) {
       
                printf("Process exists and we can send it a signal\n");
            } else {
       
                if (errno == EPERM)
                    printf("Process exists, but we don't have "
                           "permission to send it a signal\n");
                else if (errno == ESRCH)
                    printf("Process does not exist\n");
                else
                    errExit("kill");
            }
        }
    
        exit(EXIT_SUCCESS);
    }
    

    1.检查进程的存在

    检查进程是否存在方法:

    1. kill中sigo参数为0,则kill任执行正常的错误检查,但不发送信号。常用来确定特定进程是否存在。验证特定进程ID的存在并不能保证特定进程仍在运行。因为内核会随着进程的生灭而循环使用进程。所以一段时间,同一进程可能是另一进程了。特定进程可能存在但是一个僵尸进程(父进程未执行wait()来获取其终止状态)。

    2. wait()系统调用,监控调用者的子进程。

    3. 信号量和排他文件锁。

    4. 管道和FIFO之类的IPC通道。

    5. /proc/PID接口。

    五、函数alarm和pause

    //设置定时器,定时器超时产生SIGALRM
    //忽略或不捕捉此信号,默认动作是终止调用该alarm函数的进程。
    #include
    unsigned int alarm(unsigned int seconds);
    //seconds:产生信号SIGALRM需要经过的时钟秒数。
    
    //时刻到达时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需一个时间间隔。
    
    1. 每个进程只能有一个闹钟时间,

    2. 调用alarm时,之前已为该进程注册的闹钟没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值代替。

    3. 如果有以前注册的尚未超过的闹钟时间,而且本次调用的seconds值是0,则 取消以前的闹钟时间,其余留值仍作为alarm函数的返回值。

    4. SIGALRM 的默认动作是终止进程,但是大多数使用闹钟的进程捕捉 此信号。此时进程要终止,则在终止之前它可以执行所需的清理操作。

    5. 捕捉 SIGALRM 信号,则必须在调用 alarm 之前安装该信号的处理程序,若先调用 alarm,然后在我们能够安装SIGALRM处理程序之前已接 到该信号,那么进程将终止。

    //调用进程挂起直至捕捉到一个信号
    #include 
     int pause(void);
     //返回值:−1,errno设置为EINTR
     //只有执行了一个信号处理程序并从其返回时,pause才返回。
    

    使用alarm和pause,进程可使自己休眠一段指定的时间。

    六、显示信号描述

    每个信号都有一串与之相关的可打印说明。这些描述位于数组sys_siglist中。
    例:用sys_siglist[SIGPIPE]获取对SIGPIPE信号(管道端口)的描述。直接引用sys_siglist数组,不如用strsignal()函数。

    #define _BSD_SOURCE
    #include
    extrean const char *const sys_siglist[];
    #define _GNU_SOURCE
    #include
    
    char *strsignal(int sig);
    //对sig参数进行边界检查,然后返回指针。
    //指针指向针对该信号的可打印描述字符串。信号编号无效指向错误字符串。
    //除去边界检查外,函数直接引用sys_siglist数组的另一个优势对本地设置敏感,所以显示信号描述时会使用本地语言。
    
    //和strsignal函数一样,对本地设置敏感。
    #include
    void psignal(int sig,const char *msg);
    

    1.信号集

    //多个信号可使用一个称之为信号集的数据结构表示,数据类型sigset_t。
    #include   
    int sigemptyset(sigset_t *set);//初始化set指向的信号集,清除其中所有信号。
    int sigfillset(sigset_t *set); //初始化set指向的信号集,使其包含所有信号。
    
    
    int sigaddset(sigset_t *set, int signo);//添加单个信号
    int sigdelset(sigset_t *set, int signo); //删除单个信号
    //signo:表示信号编号
    //4个函数返回值:若成功,返回0;若出错,返回−1 
    
    int sigismember(const sigset_t *set, int signo);//测试信号signo是否是信号集set成员
    //signo是set一个成员,那么sigismember()函数将返回1,否则0。
    //返回值:若真,返回1;若假,返回0
    

    实现的信号数目少于一个整型所包含的位数,可用一位代表一个信号的方法实现信号集。假定一种实现有31种信号和32位整型。sigemptyset函数将整型设置为0,sigfillset函数则将整型中的各位都设置为1。定义在头文件中实现为宏:

    #define sigemptyset(ptr) (*(ptr) = 0) 
    #define sigfillset(ptr) (*(ptr) = ~(sigset_t)0, 0)
    
    1. 除了设置信号集中各位为1外,sigfillset必须返回0,所以使用C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。

    2. sigaddset 开启一位(将该位设置为 1),sigdelset 则关闭一 位(将该位设置为0);

    . sigismember测试一个指定的位。因为没有信号编号为 0,所以从信号编号中减1以得到要处理位的位编号数。

    GNU C库实现了3个非标准函数,对上述信号集标准函数的补充。

    #define _GNU_SOURCE
    #include
    
    int sigandset(sigset_t *set,sigset_t *left,sigset_t *right);//将left集合right集的交集置于dest集。
    int sigorset(sigset_t *dest,sigset_t *left,sigset_t *right);//将left集right集的并集置于dest集。
    
    int sigisemptyset(const sigset_t *set);//若set集内未包含信号,则返回true。
    

    例:

    static void sig_int(int signo)
    {
       
        printf("catch SIGINT\n");
        if ( signal(SIGINT, SIG_DFL) == SIG_ERR )
        {
       
            perror("signal\n");
        }
    }
    
    int main ( int argc, char *argv[] )
    {
       
        sigset_t newset,oldset,pendmask;
    
        if ( signal(SIGINT,sig_int) == SIG_ERR )
        {
       
            perror("signal\n");
        }
    
        if ( sigemptyset(&newset) < 0 )
        {
       
            perror("sigempty\n");
        }
    
        if ( sigaddset(&newset, SIGINT) < 0 )
        {
       
            perror("sigaddset\n");
        }
    
        if ( sigprocmask(SIG_BLOCK, &newset, &oldset) < 0 )
        {
       
            perror("sigprocmask\n");
        }
        printf("\nSIGINT block\n");
    
        sleep(5);
    
        if ( sigpending(&pendmask) < 0)
        {
       
            perror("sigpending\n");
        }
    
        if ( sigismember(&pendmask, SIGINT) )
        {
       
            printf("SIGINT is pendding\n");
        }
    
        if ( sigprocmask(SIG_SETMASK, &oldset, NULL) < 0 )
        {
       
            perror("sigprocmask\n");
        }
        printf("\nSIGINT unblock\n");
    
        sleep(5);
        return 0;
    }
    

    七、函数sigprocmask

    sigprocmask 是仅为单线程进程定义的。处理多线程进程中信号的屏蔽使用 另一个函数。

    //进程的信号屏蔽字规定当前阻塞而不能递送给该进程的信号集。
    //此函数可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。
    
    #include
    int sigprocmask(int how,const sigset_t  *set,sigset_t *oldset);
    //oldset是非空指针,指向sigset_t结构缓冲区,进程的当前信号屏蔽字通过oldset返回。
    //set是个非空指针,则参数how指示如何修改当前信号屏蔽字。
    //set是空指针,则不改变该进程的信号屏蔽字,how的值无意义。
    
    
    

    how可选参数如下图所示:
    在这里插入图片描述

    //打印调用进程信号屏蔽字中的信号名
    #include "apue.h"
    #include 
    
    void pr_mask
  • 相关阅读:
    【C++STL基础入门】list的增、删
    IDEA如何将本地项目推送到GitHub上?
    《Linux驱动:块设备的读写流程( ll_rw_block 接口分析)》
    【C++设计模式之观察者模式:行为型】分析及示例
    Java自定义注解
    SpringBoot2
    如何招到适合自己店铺的淘宝主播
    JVM 方法内联
    ELK入门(三)-Kibana
    nginx限流 漏桶与令牌桶
  • 原文地址:https://blog.csdn.net/weixin_50866517/article/details/126932872