LinuxC—信号

信号

1 信号的概念

  • 信号是软件层面的中断
  • 信号的响应依赖于中断

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xv72jW1m-1673427658676)(…/牛客项目/temp/image-20230109101013452.png)]

2 signal(2)

定义——信号处理函数,注册当前信号的行为

当signum信号到来了,执行handler行为,最终返回这个信号之前的行为

#include <signal.h>
typedef void (*sighandler_t)(int);//这个int就是信号的标识
sighandler_t signal(int signum, sighandler_t handler);

标准形式:因为C中可能有重名的风险

void (*singal(int signum, void (*func)(int)))(int);

示例1

int main(int argc, char **argv) {
   

    int i;
    //SIGINT表示终端中断,shell中的ctrl+c
    //SIG_IGN表示忽略信号
    signal(SIGINT, SIG_IGN);
    for (i = 0; i < 10; i++) {
   
        write(1, "*", 1);
        sleep(1);
    }
    exit(0);
}

输出结果:可以看到即使ctrl+c了依旧在接着输出,说明终止信号被忽略了,还有一点signal是给程序规定了接收到信号后的动作,在规定之后程序运行过程中只要接受到有关信号就会做出响应

**^C********

示例2

static void int_handler(int s) {
   
    write(1, "!", 1);
}

int main(int argc, char **argv) {
   

    int i;
    signal(SIGINT, int_handler);
    for (i = 0; i < 10; i++) {
   
        write(1, "*", 1);
        sleep(1);
    }
    exit(0);
}

输出结果:可以看到在接受到信号的时候执行了int_handler

***^C!***^C!***^C!*

注意,当我们非常快的按ctrl+c时,可以看到程序非常快就就结束了,这是因为信号会打断阻塞的系统调用,对于这个现象,比如open(), read(), write()系统调用都有可能被信号打断,当其被信号打断时会返回EINTR(<0的数),所以需要对其返回值做判断,是EINTR时需要继续执行我们的程序。

3 可重入函数

信号的不可靠

信号处理函数的执行现场是内核布置的,当多个信号连续到来时就可能出现一个问题,后面信号的现场给前面的信号处理现场覆盖掉了,这就是信号的不可靠,要解决这种问题就引入了可重入函数

可重入函数

  • 重入的概念:第一次调用还没结束就开始第二次调用,可能导致第一次调用出现一次不可预料的结果

  • 所有的系统调用都是可重入的,一部分标准库函数也是可重入的,比如一些函数会有_r的版本,其中没有_r版本的就不能够用在信号处理函数当中,防止重入的现象,localtime返回的tm*所指向的内存是静态区,如果出现重入现象就会导致原先的tm结果被覆盖,所以引入_r版本,由调用的用户来决定执行结果的存放区域即result

    struct tm *localtime(const time_t *timep);
    struct tm *localtime_r(const time_t *timep, struct tm *result);
    

    又比如memcpy函数,是可重入的

4 信号的响应过程

具体过程

首先的明白一个概念,就是调度,虽然宏观上看着好像我们某个程序一致占用着CPU在进行,但实际上在微观上程序实际上是交替着执行,只不过因为交替的非常快,导致我们看到的效果好像一个程序一致占用着CPU在执行,这个交替的策略,就是所谓的调度策略,现代CPU的调度策略通常都是时间片调度,就是说规定一个CPU时间片,某个进程CPU调度后只能执行这样的一个时间片的时间,执行完后就会响应时钟中断,讲这个进程切换到内核态后进入队列中等待下一次被CPU调度。理解了这个概念后,再来看信号响应的过程,以4.2中的函数为例:

  • main函数负责打印*
  • int_handler信号处理函数负责打印!

内核为每个进程维护了两个和信号有关的位图,分别是:

  • mask位图,信号屏蔽字,用来表示当前的信号的状态
  • pending位图,用来记录当前进程接收到了哪些信号

这两个位图中的每一位就对应了一种信号,当进程接受到一个信号的时候就会将信号对应的pending位置1,这时进程可能正在被调度或者已经在调度队列里面等着了,这里需要注意在从user态到kernel态时(该过程就是时间片用完,响应CPU调度了)进程会在自己内核中的PCB(进程控制块)中保存好自己的执行现场,比如就有执行的下一条指令的地址address。

当进程再次被CPU调度的时候,也就是要从kernel态切换到user态执行时,会将mask和pending做一个与运算,这时pending位被置1的位就会被保留,这时候就知道进程接受到哪些信号了,这时,内核会将PCB中的address换成信号处理函数的入口地址后执行信号处理函数,同时将该信号在mask和pending位图中对应的位置置0,在执行完信号处理函数(打印!)以后又会回到kernel态将address换成原先main执行到的指令地址,并将mask中对应的位置1,之后再被调度时又会经历kernel态到user态,又会做mask&pending重复上面的过程。

需要注意的点

  • 信号从收到到响应有一个不可避免的延迟,这很好理解,从上面的解释可以看出,响应信号是依赖于时钟中断的,比如我们进程在内核队列中等待被调度时,可能就接受到一个信号,但这个信号的响应必须等到该进程下一次被调度经历kernel态到user态的转换时才会进行
  • 标准信号的响应没有严格的顺序
  • 如何忽略一个信号:将该信号对应的mask位置0
  • 标准信号是会丢失的,因为标准信号用的是位图,当我们在执行一个信号处理函数时,无论这个进程又接受到多少信号都只会重复将相应的pending位置1的动作,并不会计数
  • 不能从信号处理函数中随意地往外跳转(setjmp()和longjmp()),若跳转可能导致mask对应位不再被置1从而导致该信号以后再也不会被响应。(但是sigsetjmp()和siglongjmp()可以用来跳转)

5 常用函数

5.1 函数定义

kill(2) 给pid进程发送sig信号

  • 定义
/* Send signal SIG to process number PID.  If PID is zero,
   send SIG to all processes in the current process's process group.
   If PID is < -1, send SIG to all processes in process group - PID.  */
extern int kill (__pid_t __pid, int __sig) __THROW;
  • pid的选择
    • pid > 0,就是给pid对应的进程发信号
    • pid =0,给调用该方法的进程的同组进程发送sig信号,即组内广播
    • pid = -1 ,sig会被发送给当前进程能够发送信号的所有进程,除了init进程
    • pid < -1,会将sig发送给进程组ID为-pid的进程
  • sig = 0时为检测pid进程或-pid进程组是否存在
  • 返回值:
    • 成功返回0
    • 失败返回-1,同时设置errno

raise(3) 给当前进程发送一个信号,自己给自己发

  • 定义
/* Raise signal SIG, i.e., send SIG to yourself.  */
extern int raise (int __sig) __THROW;
  • 返回值:0表示成功,非0表示失败

alarm(2) 在seconds秒后给进程发送一个SIGALRM信号

/* Schedule an alarm.  In SECONDS seconds, the process will get a SIGALRM.
   If SECONDS is zero, any currently scheduled alarm will be cancelled.
   The function returns the number of seconds remaining until the last
   alarm scheduled would have signaled, or zero if there wasn't one.
   There is no return value to indicate an error, but you can set `errno'
   to 0 and check its value after calling `alarm', and this might tell you.
   The signal may come late due to processor scheduling.  */
extern unsigned int alarm (unsigned int __seconds) __THROW;

注意,当连续使用多个alarm()时会生效最后一个

pause(2)

​ 等待直到有一个信号来打断

/* Suspend the process until a signal arrives.
   This always returns -1 and sets `errno' to EINTR.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pause (void);

sleep(3)

​ 阻塞进程seconds后唤醒进程,或在sleep途中被信号打断,该函数轻易不能使用,因为在有些环境下该函数就是用alarm() + pause()来封装的,当程序中有多个alarm时是可能会出错的

/* Make the process sleep for SECONDS seconds, or until a signal arrives
   and is not ignored.  The function returns the number of seconds less
   than SECONDS which it actually slept (thus zero if it slept the full time).
   If a signal handler does a `longjmp' or modifies the handling of the
   SIGALRM signal while inside `sleep' call, the handling of the SIGALRM
   signal afterwards is undefined.  There is no return value to indicate
   error, but if `sleep' returns SECONDS, it probably didn't work.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern unsigned int sleep (unsigned int __seconds);

可以用nanosleep()/usleep()/select()函数来代替sleep()

setitimer(2) 设置一个时钟

设置一个时钟,当old不会空时将原来时钟的信息回填到old中,将new中的时间都设置为0即可取消定时器

/* Set the timer WHICH to *NEW.  If OLD is not NULL,
   set *OLD to the old value of timer WHICH.
   Returns 0 on success, -1 on errors.  */
extern int setitimer (__itimer_which_t __which,
            const struct itimerval *__restrict __new,
            struct itimerval *__restrict __old) __THROW;
  • which表示哪个时钟,共有三种

    • ITIMER_REAL 实时(realtime)递减,直到为0时发送一个SIGALRM信号,此时可以代替alarm()函数
    • ITIMER_VIRTUAL 当进程运行时(usertime)递减,到0时发送一个SIGVTALRM信号
    • ITIMER_PROF 递减usertime+kerneltime,到0时发送一个SIGPROF信号
  • itimerval结构体

struct itimerval {
   
    struct timeval it_interval; /* 递减到0时将这个值赋给value且操作时原子的,即该函数自己就能构成一个										时钟周期*/
    struct timeval it_value;    /* 递减的就是这个值*/
};

 struct timeval {
   
     time_t      tv_sec;         /* seconds */
     suseconds_t tv_usec;        /* microseconds 微秒*/
 };
  • 返回值:0表示成功,-1表示失败并设置errno的值

abort(3)

给当前进程发送一个SIGABRT,目的是为了结束当前进程

/* Abort execution and generate a core-dump.  */
extern void abort (void) __THROW __attribute__ ((__noreturn__));

system()

在可能有信号干扰的情况下使用这个函数需要注意将SIGCHLD信号block,并将SIGINT和SIGQUIT信号忽略

5.2 函数计时比较

计算5s中能将一个数++多少次

  • 使用time函数来实现
int main(int argc, char **argv) {
   

    time_t end;
    end = time(NULL) + 5;
    int64_t cnt = 0;

    while (time(NULL) <= end) cnt++;

    printf("%ld\n", cnt);

    exit(0);
}

​ 输出结果:

root@VM-24-2-ubuntu:/home/ubuntu/linux# time ./main
2062958242

real	0m5.401s
user	0m5.400s
sys	0m0.000s
  • 使用alarm+信号来实现
int main(int argc, char **argv) {
   

    alarm(5);
    int64_t cnt = 0;

    while (1) {
   
        cnt++;
    }

    exit(0);
}

​ 输出结果

root@VM-24-2-ubuntu:/home/ubuntu/linux# time ./main
2859820977
real	0m5.001s
user	0m4.997s
sys	0m0.004s

可以看到使用信号来实现时时间更加精确,同时cnt++执行的次数也更多了

static volatile int loop = 1;

static void alarm_handler(int s) {
   
        loop = 0;
}

int main(int argc, char **argv) {
   
    //注意signal得再alarm前面
	signal(SIGALRM, alarm_handler);
    int64_t cnt = 0;
    alarm(5);
    
    
    while (loop) {
   
        cnt++;
    }
    printf("%ld", cnt);

    exit(0);
}

5.3 漏桶实现

实现一个shell中的cat命令,要求每秒只能读取10个字符

#define CPS 10
#define BUFSIZE CPS

static volatile int loop = 0; //控制时延的标志

static void alrm_handler(int s) {
   
    alarm(1);
    loop = 1;
}

/**
 * 实现一个cat,且每秒读取CPS个字符
 */
int main(int argc, char **argv) {
   

    if (argc < 2) {
   
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    int fds, fdd = 1, len, ret, pos;
    char buf[BUFSIZE];

    //打开待读取的文件
    do {
   
        fds = open(argv[1], O_RDONLY);
        if (fds < 0) {
   
            if (errno == EINTR) continue;
            perror("open():");
            exit(1);
        }
    } while (fds < 0);

    //向终端上写数据
    //首先注册信号函数
    alarm(1);
    signal(SIGALRM, alrm_handler);

    while (1) {
   
        while (!loop) pause();
        loop = 0
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值