linux系统编程03-并发:信号

介绍

Untitled

Untitled

1. 信号的概念

信号是软件中断

信号的实现依赖中断

Untitled

前31个:标准信号; 34~64:实时信号

标准信号会丢失,实时信号不会

ulimit -c 产生core文件

报段错误产生core文件,然后gdb调试core文件

2. signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

//相当于: (防止命名空间冲突)
 void **(*** signal(int signum, void (*function) (int)) **)** (int)

注册信号处理函数

  • 参数:响应的信号(用宏更好); 处理函数 或者 SIG_IGN[忽略] , SIG_DFL[默认]
  • 返回值:以前定义的旧行为

例子: ctrl+c 发送 SIGINT 信号,注册新的处理函数

示例代码:

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

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

int main()
{
    int i;
    
    signal(SIGINT, int_handler);

    for(i=0; i<10; i++)
    {
        write(1,"*",1);
        sleep(1);
    }

    exit(0);
}

运行结果:

Untitled

问题:

快速按下 ctrl+c,程序执行时间不足10s

Untitled

之前学过的所有io都是阻塞io,所以出错时都要考虑是否是信号错,如果是,相当于是假错,重试,否则才报错

open\read\write的errno都有 EINTR ,说明都会被信号打断。可以这么修改:先判断,如果真错就退出,假错就重试。

Untitled

3. 信号的不可靠性

行为不可靠,处理函数没有手动调用,所以是内核布置现场,因此同一时间内一个信号出现多次,后面响应信息可能会把前面的覆盖。

4. 可重入函数

解决上述问题:

Untitled

一个函数有 _r 版本,那么前一个版本必然不能用作信号处理函数。

同时说明前一个版本的指针必然是指到了 static 静态区(因为会互相覆盖):

Untitled

_r 版本 成为可重入的方法是调用时传入了地址空间,避免了数据冲突

下面的函数函数同理:

Untitled

5. 信号的响应过程:过程图

Untitled

Untitled

  1. 内核为进程维护了一组位图,mask和pending,是32位的,对应32个标准信号。

    进程在扎入内核等待调度的时候,会保存执行现场,包括返回地址address

  2. 当进程被调度,从kernel返回到user的时候,会 mask & pending ,哪一位为1,说明哪一个信号到来

  3. 知道哪个信号到来后,执行信号处理函数:将 address 修改为信号处理函数的入口地址,将mask和pending的对应位置为0,然后跳转到信号处理函数

  4. 执行完信号处理函数后,将mask置为1,再次检查 mask & pending

为什么会有不可避免的延迟:信号的实现依赖与中断扎内核,从内核出来的时候才会检测信号,所以又延迟。

为什么会丢失: 因为是位图,所以一个信号来100次,置100次1,还是1

如何忽略一个信号: 设置SIG_IGN[忽略] 把 mask 对应位置为0

不能从信号处理函数随意往外跳:会错过第二次扎内核的过程,导致 mask 没有从0恢复到1,可以用 sigsetjmpsiglongjmp 解决。

细化到线程维度:  实际进行两次,一次和进程pending,一次和线程pending

细化到线程维度: mask & pending 实际进行两次,一次和进程pending,一次和线程pending

6. 常用函数

kill

#include <sys/types.h>
#include <signal.h>
//给某个进程发信号
int kill(pid_t pid, int sig);
  • pid_t
    • 0:指定的进程

    • 0:组内广播
    • -1:全局广播
    • -x:第x组的进程

Untitled

  • 返回值:0成功,非0失败。错误分为三种:

    Untitled

raise

#include <signal.h>
//给当前进程发信号
int raise(int sig);
  • 等价于: kill(getpid(), sig)pthread_kill(pathread_self(), sig)

alarm\pause

#include <unistd.h>
//等待 secondes 秒,发送SIGALRM 信号(默认行为是终止进程)
unsigned int alarm(unsigned int seconds);
  • 无法实现多任务,重复设置谁先生效听谁的。
  • time() 函数更精确,ms级别
#include <unistd.h>
//sleep,知道被一个信号唤醒,防止忙等待
int pause(void);

例子:5sec.c

示例代码:

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<time.h>

int main()
{
    long long  cnt = 0;
    time_t end;

    end = time(NULL)+5;

    while(time(NULL)<end)
        cnt++;
    printf("%lld\n", cnt);

    exit(0);
}
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

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

int main()
{
    long long  cnt = 0;

    signal(SIGALRM, alarm_handler);
    alarm(5);

    while(loop)
        cnt++;
    printf("%lld\n", cnt);

    exit(0);
}

运行结果:

Untitled

  • 其他问题:加上O1优化:死循环无法退出

    Untitled

    前

    后:死循环

    后:死循环

    循环里没用到loop,优化认为loop不变,于是直接取值,没有去loop的地址里取值,loop恒为1

    Untitled

    volitale作用是声明该变量可能由于其他外部环境所改变

    Untitled

漏桶和令牌桶

例子:修改mycopy(目标设为fd1标准输出)为slowcat。每秒读取10个字符,alarm用作流量控制[sleep移植性不足]

漏桶:娟娟溪流,无法应对突发大数据

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>

##define CPS 10
##define BUFSIZE 1024

static volatile int loop = 0;
static void alarm_handler(int s)
{
		//链式调用,一环接一环
    alarm(1);
    loop = 0;
}

int main(int argc, char **argv)
{
    if(argc < 2)
    {
        fprintf(stderr, "Usage:...\n");
        exit(1);
    }

		//输出位置为fd1
    size_t sfd, dfd=1;
    int len=0, ret, pos;
    char buf[BUFSIZE];

		//设置信号处理函数
    signal(SIGALRM, alarm_handler);
    alarm(1);

    do
    {
        sfd = open(argv[1], O_RDONLY);
        if(sfd < 0)
        {
            //出假错:被信号打断,则重试
            if(errno != EINTR)
            {
                perror("open()");
                exit(1);
            }
        }
    }while(sfd < 0);

    while(1)
    {
				//避免忙等待
        while(loop)
            pause();

        loop = 1;
        
        //读取10个字节 
        while((len = read(sfd, buf, CPS)) < 0)
        {
            if(len < 0)
            {
								//出假错:被信号打断,则重试
                if(errno != EINTR)
                {
                    perror("read()");
                    close(sfd);
                    exit(1);
                }
            }
            if(len == 0)
                break;
        }

        pos = 0;
        while(len > 0)
        {
            ret = write(dfd, buf+pos, len);
            if(ret < 0)
            {
                perror("write()");
                break;
            }
            pos += ret;
            len -= ret;
        }
    }

    close(sfd);

    exit(0);
}

运行结果:

没用pause,cpu沾满

没用pause,cpu沾满

用了pause就不会

用了pause就不会

注意点:

  • alarm链式调用和处理函数的书写
  • while((len = read(sfd, buf, CPS)) < 0) 当没读到内容的时候就重新尝试读。

令牌桶:可以处理突发数据

写法token

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>

##define CPS 10
##define BUFSIZE 1024
##define BRUST 100

static int token = 1;
static void alarm_handler(int s)
{
    alarm(1);
    token++;
    if(token > BRUST)
        token = BRUST;
    
}

int main(int argc, char **argv)
{
    if(argc < 2)
    {
        fprintf(stderr, "Usage:...\n");
        exit(1);
    }

    size_t sfd, dfd=1;
    int len=0, ret, pos;
    char buf[BUFSIZE];

    signal(SIGALRM, alarm_handler);
    alarm(1);

    do
    {
        sfd = open(argv[1], O_RDONLY);
        if(sfd < 0)
        {
            //出假错:被信号打断,则重试
            if(errno != EINTR)
            {
                perror("open()");
                exit(1);
            }
        }
    }while(sfd < 0);

    while(1)
    {
        while(token < 0)
            pause();
        token --;
        
        //读取10个字节 
        while((len = read(sfd, buf, CPS)) < 0)
        {
            if(len < 0)
            {
                if(errno != EINTR)
                {
                    perror("read()");
                    close(sfd);
                    exit(1);
                }
            }
            if(len == 0)
                break;
        }

        pos = 0;
        while(len > 0)
        {
            ret = write(dfd, buf+pos, len);
            if(ret < 0)
            {
                perror("write()");
                break;
            }
            pos += ret;
            len -= ret;
        }
    }

    close(sfd);

    exit(0);
}

运行结果:同上

思路:处理突发数据体现在哪里

while((len = read(sfd, buf, CPS)) < 0) 当没读到内容的时候重新尝试读,并且token++,相当于积攒令牌,以后有大数据来的时候,就可以连续读取多次。

问题: token- - 不一定原子【信号原子类型】

Untitled

令牌桶封装成库

文件

  • main.c :模拟应用实例,以 slowcat1.c (令牌桶) 为基础
  • mytbf.c :库文件,动态连接到应用程序,实现令牌桶
  • mytbf.h :头文件

基本思路:

令牌桶数据作为一个结构体存放到到数组中,结构体内容包括 cps,burst,token

给用户提供四个库函数

  • mytbf_t * mytbf_init(int cps, int brust); 初始化令牌桶,成功放回令牌桶结构体指针,失败返回 NULL
  • int mytbf_fetchtoken(mytbf_t *ptr, int size); :希望从令牌桶中拿到size个令牌(令牌:字符=1:1),返回实际拿到的值 min(token, size);没有令牌则阻塞。
  • int mytbf_returntoken(mytbf_t *ptr, int size); :归还size个令牌。
  • int mytbf_destroy(mytbf_t *ptr); :销毁当前令牌桶

Untitled

示例代码:

##ifndef MYTBF_H__
##define MYTBF_H__

##define MYTBF_MAX 1024
//隐藏定义,真正的定义在mytbf.c中
typedef void mytbf_t;

mytbf_t * mytbf_init(int cps, int brust);

int mytbf_fetchtoken(mytbf_t *, int);

int mytbf_returntoken(mytbf_t *, int);

int mytbf_destroy(mytbf_t *);

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

#include<string.h>
#include<errno.h>

#include "mytbf.h"

struct mytbf_t
{
    int cps;
    int burst;
    int token;
    int pos;
};

typedef void (*sighandler_t)(int);
static struct mytbf_t* job [MYTBF_MAX];
static int inited = 0;
static sighandler_t alarm_handler_save;

static void alarm_handler(int s)
{
    int i;

    //alarm链式调用
    alarm(1);

    for(i = 0; i < MYTBF_MAX; i++)
    {
        if(job[i] != NULL)
        {
            job[i]->token += job[i]->cps;
            if(job[i]->token > job[i]->cps)
                job[i]->token = job[i]->cps;
        }
    }
}

//模块卸载:在进程结束的时候调用,钩子函数
static void module_unload(void)
{
    int i;
    //恢复信号处理函数,关闭alarm信号
    signal(SIGALRM, alarm_handler_save);
    alarm(0);
    for(i = 0; i < MYTBF_MAX; i++)
        free(job[i]);
}

static void module_load(void)
{
    alarm_handler_save = signal(SIGALRM, alarm_handler);
    alarm(1);

    //挂钩
    atexit(module_unload);
}

static int get_free_pos(void )
{
    int i;
    for(i = 0; i < MYTBF_MAX; i++)
    {
        if(job[i] == NULL)
            return i;
    }
    return -1;
}

mytbf_t *mytbf_init(int cps, int burst)
{
    struct mytbf_t *me;
    int pos;
    
    //alarm:注册和发送只能调用一次
    if(!inited)
    {
        module_load();
        inited = 1;
    }

    me = malloc(sizeof(*me));
    if(me == NULL)
        return NULL;
    pos = get_free_pos();
    if(pos < 0)
        return NULL;
    
    me->token = 0;
    me->cps = cps;
    me->burst = burst;
    me->pos = pos;
    
    job[pos] = me;

    return me;
}

static int min(int a, int b)
{
    if(a < b)
        return a;
    else 
        return b;
}

int mytbf_fetchtoken(mytbf_t *ptr, int size)
{
    struct mytbf_t *me = ptr;
    int n;
    if(size < 0)
        return -EINVAL;
    //阻塞:一直等到有token位置才继续
    while(me->token <= 0)
        pause();
    n = min(me->token, size);
    me->token -= n;

    return n;
}

int mytbf_returntoken(mytbf_t *ptr, int size)
{
    struct mytbf_t *me = ptr;
    if(size <= 0)
        return -EINVAL;
    me->token += size;
    
    if(me->token > me->burst)
        me->token = me->burst;

    return size;
}

int mytbf_destroy(mytbf_t *ptr)
{
    struct mytbf_t *me = ptr;

    job[me->pos] = NULL;
    free(ptr);
    
    return 0;
    free(me);
}
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<string.h>
#include"mytbf.h"

##define CPS 10
##define BUFSIZE 1024
##define BURST 100

/*static int token = 1;
static void alarm_handler(int s)
{
    alarm(1);
    token++;
    if(token > BRUST)
        token = BRUST;
    
}*/

int main(int argc, char **argv)
{
    if(argc < 2)
    {
        fprintf(stderr, "Usage:...\n");
        exit(1);
    }

    size_t sfd, dfd=1;
    int len=0, ret, pos, size;
    char buf[BUFSIZE];
    mytbf_t *tbf;

   /* signal(SIGALRM, alarm_handler);
    alarm(1);*/
    tbf = mytbf_init(CPS, BURST);
    if(tbf == NULL)
    {
        fprintf(stderr, "mytbf_init() failed!\n");
        exit(1);
    }

    do
    {
        sfd = open(argv[1], O_RDONLY);
        if(sfd < 0)
        {
            if(errno != EINTR)
            {
                perror("open()");
                exit(1);
            }
        }
    }while(sfd < 0);

    while(1)
    {
        /*while(token < 0)
            pause();
        token --;*/
        
        //拿取令牌
        size = mytbf_fetchtoken(tbf, BUFSIZE);
        if(size < 0)
        {
            fprintf(stderr,"mytbf_fetchtoken():%s\n", strerror(-size));
             exit(1);
        }

        while((len = read(sfd, buf, size)) < 0)
        {
            if(len < 0)
            {
                if(errno != EINTR)
                {
                    perror("read()");
                    close(sfd);
                    exit(1);
                }
            }
            if(len == 0)
                break;
        }

        pos = 0;

        //归还token
        if(len-size > 0)
            mytbf_returntoken(tbf, size-len);

        while(len > 0)
        {
            ret = write(dfd, buf+pos, len);
            if(ret < 0)
            {
                perror("write()");
                break;
            }
            pos += ret;
            len -= ret;
        }
    }

    close(sfd);
    mytbf_destroy(tbf);
    
    exit(0);
}
all:mytbf

mytbf:main.o mytbf.o
                gcc $^ -o $@

clean:
        rm -rf *.o mytbf

运行结果:

Untitled

setitimer:替代alarm

同样是定点发送 SIGALRM :替代alarm,更好用,不需要链式使用,不会累计误差。

#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
             struct itimerval *old_value);

Untitled

  • 参数
    • itimerval : 两个成员,都是 timeval 类型的结构体, it_interval 会在 it_value 到时的时候重新填充 it_value
    • new_value 是我们填入的计时参数, old_value 用来回填原来的计时参数,类似于 signal 返回原理的信号处理函数
  • 返回值:成功0,失败-1

例子:setitimer替换alarm

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/time.h>

int main()
{

    //alarm(1);
    struct itimerval itv;
    
    itv.it_interval.tv_sec = 1;
    itv.it_interval.tv_usec = 0;
    itv.it_value.tv_sec = 1;
    itv.it_value.tv_usec = 0;
    if(setitimer(ITIMER_REAL, &itv, NULL) < 0)
    {
        perror("setitimer()");
        exit(1);
    }

    while(1)
        pause();

    exit(0);
}

运行结果:

Untitled

其他

Untitled

Untitled

7. 信号集:sigemptyset

Untitled

8. 信号屏蔽字/pending集的处理:sigprocmask

sigprocmask相当于给 pending 数组置位, sigprcmask(SIG_SETMASK, sigset_t oset, xxx) 相当于把 oset 代表的当前 pending 数组赋给当前进程

Untitled

无法控制信号的到来,但可以控制什么时候响应

Untitled

Untitled

Untitled

Untitled

Untitled

9. 拓展内容

sigsuspend

#include <signal.h>
int sigsuspend(const sigset_t *mask);
  • 作用:等待信号集里的一个信号
  • 流程:
    1. 设置 mask
    2. 阻塞,等待信号到来
    3. 恢复为原来的 mask

Untitled

例子:信号驱动程序【pause实现】

示例代码:

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

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

int main()
{
    int i,j;
    sigset_t set, oset, saveset;

    signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set,SIGINT);

    sigprocmask(SIG_UNBLOCK, &set, &saveset);
    for(i = 0; i < 1000; i++)
    {
        sigprocmask(SIG_BLOCK, &set, &oset);
        for(j = 0; j < 5; j++)
        {
            write(1,"*",1);
            sleep(1);
        }
        write(1,"\n",1);
        sigprocmask(SIG_SETMASK, &oset, NULL);
        pause();
    }
    sigprocmask(SIG_SETMASK, &saveset, NULL);

    exit(0);
}

运行结果:

ctrl + \ 发送 quit 信号退出

ctrl + \ 发送 quit 信号退出

思路:

加上 puase 就可以做出信号驱动程序的样子,但是我们信号打断打印 * 的系统调用,所以把信号阻塞了(pending = 1 但由于 mask = 0,所以收到但不响应)。

存在问题:

sigprocmask(SIG_UNBLOCK, &set, NULL); pause(); 两个操作不原子: sigprocmask 也是系统调用,导致它扎内核回来后,检查mask&pending,发现有信号来,直接去响应了,相应完后pending=0, pause 没有察觉,即没来得及 pause 就直接把信号响应掉了。

例子:信号驱动程序【suspend】实现

示例代码:

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

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

int main()
{
    int i,j;
    sigset_t set, oset, saveset;

    signal(SIGINT, int_handler);
    sigemptyset(&set);
    sigaddset(&set,SIGINT);

    sigprocmask(SIG_UNBLOCK, &set, &saveset);
    //多了这一句话:类似do while
    sigprocmask(SIG_BLOCK, &set, &oset);
    for(i = 0; i < 1000; i++)
    {
        for(j = 0; j < 5; j++)
        {
            write(1,"*",1);
            sleep(1);
        }
        write(1,"\n",1);
        sigsuspend(&oset);
        
        // 相当于下面的原子操作:
        /*sigset_t tmpset;
        sigprocmask(SIG_SETMASK, &oset, &tmpset);
        pause();
        sigprocmask(SIG_SETMASK, &tmpset, &oset);
        */
    }
    sigprocmask(SIG_SETMASK, &saveset, NULL);

    exit(0);
}

运行结果:

信号不会再 sigprocmask和pause之间被响应掉,导致pause无法收到信号,以至于阻塞

Untitled

sigaction:替代signal

Untitled

信号处理函数的参数是为了区分注册信号

Untitled

Untitled

有重入危险,类似于嵌套中断

Untitled

Untitled

Untitled

signal不区分信号来源

使用三参信号处理函数

Untitled

Untitled

Untitled

10. 实时信号

与系统信号的区别

  • 不丢失
  • 响应顺序未定义

例子:用实时信号重构 suspend.c ,验证实时信号不丢失

示例代码:

运行结果:

Untitled

kill -l 查看所有信号,前34个是系统信号

Untitled

kill -x process 向某进程发送 x 信号,默认是 -9 SIGKILL 信号

ulimit -a 查看实时信号的最大排队数量

Untitled

11. 总结:信号安全

信号是先被使用,后成为事实标准,所以肯定会有边角无法纳入标准,所以使用的时候要比较注意。

如:

因此只能使用一些简单的数值计算和库函数,但是多线程没有这种问题,因为它已经先标准化后进行实现。

posted @ 2025-09-17 00:02  Miaops  阅读(9)  评论(0)    收藏  举报