LINUX系统编程-- 5 信号

LINUX系统编程-- 5 信号

五 信号

并发分两种:信号(进程间通信)与线程

内容:


1、信号的概念
2、signal
3、信号的不可靠
4、可重入函数
5、 信号的响应过程
6、常用函数:kill、raise、alarm、pause、abort、system、sleep等
7、信号集
8、 信号屏蔽字、peding集的处理
9、sigsuspend()、sigaction()、setitimer
10、实时信号


1信号的概念和相关概念

并发概念。
信号是初步异步、线程是强烈异步
同步概念
异步概念

异步事件的处理:查询法,通知法

信号是软件中断。信号的响应依赖于中断。

1、 kill -l 查看全部的信号,是shell命令

1-31是标准信号。之后的是实时信号

2 core文件:一个程序的某一个现场

默认下,是不产生core文件的:
LINUX系统编程-- 5 信号
可知默认设置下,允许的core文件的大小是0。也就是不产生core文件。
我们可以用ulimit -c 10240 来设置core文件的最大为10M。
当执行一个程序出错时,就会有core文件的产生。core文件后面的数字是进程号。
LINUX系统编程-- 5 信号

3 signal函数

函数原型是:
void(*signal(int signum,void(*func)(int)))(int)

4ctrl+c是SIGINT的快捷方式(2号信号)

5 实例

例一:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main()
{
    int i;

    signal(SIGINT,SIG_IGN);

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

这个程序使得ctrl+c不再管用,因为忽略掉了这个信号。

例二:

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

#define N 20

void handler(int sig){
    write(1,"!",1);
}


int main()
{
    int i;

    signal(SIGINT,handler);

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

注意点:

  • signal函数最需要注意的就是,回调函数!当signal函数的第二个参数是函数地址指向信号处理函数的时候,这时的信号处理函数是一个回调函数。signal只是起到了对这个信号处理函数的注册作用!!当有指定的信号发生的时候,信号处理函数才会执行。(信号处理函数执行的一个前提就是,信号发生时,程序未结束!)
    回调函数一般放在程序的开头,声明和定义放在一块。
  • 信号会打断阻塞的系统调用!!!!以open为例,当open打开一个很慢的设备的时候,open是在阻塞的,这个时候会被信号打断,打断的结果是,返回-1,并设置error为EINTR!read、write函数也是同理,当读数据或者写数据的时候,如果是阻塞中,也是会被信号打断的(同样也是将errno设置为EINTR)!
  • 通常来讲,我们将系统调用被信号打断称为假错,也就是说,这不是程序本身的问题,所以通常我们会在出错处理上加一个出错判断。
    LINUX系统编程-- 5 信号

6 信号的不可靠

指的是信号的行为不可靠。
第一次调用还没结束,第二次调用就开始了,第二次调用用的环境是第一次调用还没调用完的环境,所以会出问题。
待补充。。
。。
。。
。。
。。

7 可重入函数

是解决信号的不可靠的一种方法!
可重入函数就是说,当第一次信号处理函数还没执行完时,若又来了一个信号,那么会先把第一次的信号处理函数执行完。(而忽略第二次的信号)。也就是说,信号处理函数第一次调用没结束,发生第二次调用 但不会出错

可重入函数都是安全的,就如“可重入”的意思一样,如果某个函数是非可重入函数,那么此函数不能用于信号处理函数中。通常这个信号会提供第二个版本(通常是后面有_r),

所有的系统调用都是可重入的!一部分库函数也是可重入的,比如说:memcpy

8 信号的响应过程

LINUX系统编程-- 5 信号
LINUX系统编程-- 5 信号

LINUX系统编程-- 5 信号

LINUX系统编程-- 5 信号

9 常用函数

1、 kill函数

kill()-需要注意的就是第一个参数,可以取正负或者0,各自有不同的含义。

  • pid>0 给指定进程发送信号
  • pid==0 组内广播,即给所有同组进程发送信号
  • pid==-1 全局广播,给所有自己有权限发送信号的进程 发送信号,除init进程外,不包括init进程。一般就是init进程会发全局广播。
  • pid<-1 发送给指定进程组。将信号发送给 进程组ID绝对值 == pid绝对值的 进程组中的每个进程
  • sig == 0 不会发送任何信号,用于检测一个进程或者进程组 是否存在。

返回值: 0成功 -1失败
如果失败,查看errno 如下:

  • EINVAL 表示目标信号无效
  • EPERM 表示 进程没有向任何目标进程发送信号的权限,即表示 目标进程或进程组存在,但是你没有发送信号的权限!!
  • ESRCH pid或进程组不存在。注意,现有的进程可能是僵死进程,即已经提交了终止请求,但还没有被等待(2)的进程。

2、raise()
给当前进程发送信号,自己给自己发信号

在多进程中,相当于:

kill(getpid(), sig);

在多线程中:相当于:

pthread_kill(pthread_self(), sig);

3、alarm()
设置一个发送信号的闹钟;
alarm()以秒为单位安排将一个SIGALRM信号发送给调用进程。 SIGALRM 也是终止信号。

alarm例子:
例一:使用单一计时器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	alarm(5);
	while(1);
	exit(0);
}
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ gcc alarm.c 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ ./a.out 
Alarm clock
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$   延迟5秒

例二:alarm的连续设置 最有一个alarm有效

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	alarm(5);
	alarm(10);
	alarm(1);
	while(1);
	exit(0);
}

mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ gcc alarm.c 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ ./a.out 
Alarm clock
只延迟1秒
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$  

例三:
在alarm这里需要注意的是,当要改变sigalarm的默认行为的时候,signal 一定要写在 alarm前面,如果alarm写在前面,如下:

alarm(5);
       ... 中间程序 经过了5秒
signal(SIGALRM,alarm_handler);

这种情况 中间程序 做了5秒以上的工作,那么5秒后 当此时信号来了,程序还没有执行到 signal() 也就看不到 给信号注册的新的行为:alarm_handler,那么就会沿用这个信号默认的行为,即终止程序。

所以正确写法是:

/ signal 一定要写在 alarm前面
signal(SIGALRM,alarm_handler);
	alarm(5);

4、pause函数
等待一个信号,即这是人为做出来的一个阻塞的系统调用。(我们专门做出来的一个用来被打断的 阻塞的系统调用)。
pause()使调用进程(或线程)处于休眠状态,直到发出终止进程或调用信号捕捉函数的信号。

  • 这个函数需要注意的就是他是使进程处于休眠状态,即不在占用CPU资源,而while(1),虽然效果与pause一样,但是while使CPU一直在运行!

5、补充
在有些环境下 sleep() 是由 alarm() + pause()封装的。有些是用nanosleep()封装的。

所以 不建议使用 sleep(),理由是在 sleep() 是由 alarm() + pause()封装的环境中,当你程序中同时使用 sleep() 和 alarm()的时候,必然有覆盖一个 alarm,即 alarm() 和 sleep()当中必然会有一个失效!!当前我们使用的环境用sleep不会有任何问题,因为他是用nanosleep封装的,但是为了保证程序的可移植性,还是尽量避免使用sleep。

6、alarm实例:

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

static int loop = 1;

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

int main()
{
	int64_t count = 0;

// signal 一定要写在 alarm前面
signal(SIGALRM,alarm_handler);
	alarm(5);
	
	while(loop)
		count++;

	printf("%ld\n",count);
	exit(0);
}



mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ gcc alarm.c 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ time ./a.out 
2629709698

real	0m5.002s
user	0m4.936s
sys	0m0.000s
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 

补充:

  • alarm比time的精度高。
  • volatile 关键字:去到这个变量真正的存储空间取数值,而不是根据局部结构判断取值

9.1 漏桶实例,流量控制

实现一个cat,这个mycat与cat不同的点是每秒cat10个字符 输出到标准输出。
读取一个视频、一个音频等都是要进行流量控制的!数据不是一股脑全读完的。

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

#define BUFSIZE 10

static volatile int loop = 0;

static void alrm_handler(int s)
{
	alarm(1);//重新定时
	loop = 1;
}

int main(int argc,char *argv[])
{
	int sfd,dfd=1;
	char buf[BUFSIZE];
	int len,ret,pos;


	if(argc < 2)
	{
		fprintf(stderr,"Usage:%s <src_file> <dest_file>\n",argv[0]);
		exit(1);
	}

	signal(SIGALRM,alrm_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(),while()后 执行空,但是这样 CPU 占用率会很高,一秒钟会在这里执行循环上亿次,所以用pause()替换,直接休眠等待信号来唤醒
/*		
while(!loop)
			;
*/
		while(!loop)
			pause();
		loop = 0;

		while((len = read(sfd,buf,BUFSIZE)) < 0)
		{	
			if(errno == EINTR)//防止是 信号打断阻塞的系统调用
				continue;
			perror("read()");
			break;
		}

		if(len == 0)
			break;

		//确保写进去 len 个字节
		pos = 0;
		while(len > 0)
		{
			ret = write(dfd,buf+pos,len);
			if(ret < 0)
			{
				if(errno == EINTR) //防止是 信号打断阻塞的系统调用
					continue;
				perror("write()");
				exit(1);

			}
			pos += ret;
			len -= ret;

		}

	}

	close(sfd);

}

缺点:如果读取的是打印机类的设备,并且当时打印机上面没有数据,那么程序就会一直循环于 read处.

while((len = read(sfd,buf,BUFSIZE)) < 0)
{
if(errno == EINTR)//防止是 信号打断阻塞的系统调用
continue;

问题是这几句,一直循环于:
读阻塞 ,打断阻塞 判断是假错误(信号打断阻塞的系统调用) 返回重新读 。。。。
读阻塞 ,打断阻塞 判断是假错误(信号打断阻塞的系统调用) 返回重新读 。。。。
读阻塞 ,打断阻塞 判断是假错误(信号打断阻塞的系统调用) 返回重新读 。。。。

所以漏桶的缺陷就是 如果没有数据的时候就会一直循环等待,直到有数据,如果忽然来的数据量很大,也不能快速的去读数据,只能慢慢的一秒10个字节的去读n次

9.2 令牌桶,流量控制

令牌桶的优势,就是当没有数据可读的时候,会积攒自己的权限,意思是 如果之前30秒一直没有数据,读空了30秒,那么就存下30个权限,等到有数据的时候,快速使用前面30个权限,快速连续读30次。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
 
#define CPS 10
#define BUFSIZE CPS
#define BURST 100

static volatile int token = 0;

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

int main(int argc,char *argv[])
{
	int sfd,dfd=1;
	char buf[BUFSIZE];
	int len,ret,pos;

	if(argc < 2)
	{
		fprintf(stderr,"Usage:%s <src_file> <dest_file>\n",argv[0]);
		exit(1);
	}
	

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

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

		}
	}while(sfd < 0);


	while(1)
	{

		while(token <= 0)
			pause();
		token--;

		while((len = read(sfd,buf,BUFSIZE)) < 0)
		{	if(errno == EINTR)//signal
				continue;
			perror("read()");
			break;
		}
		if(len == 0)
			break;

		//确保写进去 len 个字节
		pos = 0;
		while(len > 0)
		{
			ret = write(dfd,buf+pos,len);
			if(ret < 0)
			{
				if(errno == EINTR) //signal
					continue;
				perror("write()");
				exit(1);
			}
			pos += ret;
			len -= ret;
		}

	}
	
	close(sfd);
		
}

9.3 令牌桶封装成库实例

将上述的令牌桶流量控制的一些细节做进一步优化,并将上述的机制封装成一个库,供后续直接使用。

//mytbf.h
#ifndef MYTBF_H__
#define MYTBF_H__

#define MYTBF_MAX 1024

typedef void mytbf_t;

mytbf_t *mytbf_init(int cps,int burst);

//获取token
int mytbf_fetchtoken(mytbf_t *,int);
//归还token
int mytbf_returntoken(mytbf_t *,int);

int mytbf_destroy(mytbf_t *);

#endif
//mystf.c
#include <asm-generic/errno-base.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#include "mytbf.h"

struct mytbf_st{
    int csp;
    int burst;
    int token;
    int pos;//任务列表的下标
};

static struct mytbf_st *job[MYTBF_MAX];
static volatile int inited = 0;
static void (*alarm_status)(int);

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


//信号处理函数
static void handler(int sig){
    alarm(1);
    for (int i = 0;i < MYTBF_MAX;i++){
        if (job[i] != NULL){
            job[i]->token += job[i]->csp;
            if (job[i]->token > job[i]->burst){
                job[i]->token = job[i]->burst;
            }
        }
    }
}

//装载信号处理模块
static void mod_load(){
    alarm_status = signal(SIGALRM,handler);//保存alarm信号处理函数原来的状态
    alarm(1);
}
//卸载信号处理模块 当发生异常退出时 可以将占用的资源释放 将alarm信号取消
static void mod_unload(){
    signal(SIGALRM,alarm_status);
    alarm(0);
    for (int i = 0;i < MYTBF_MAX;i++){
        free(job[i]);
    }
}

mytbf_t *mytbf_init(int cps,int burst){
    struct mytbf_st *tbf;

    if (!inited){
        mod_load();
    }

    //将新的tbf装载到任务组中
    int pos;
    pos = get_free_pos();
    if (pos == -1){
        return NULL;
    }

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

    return tbf;
}

//获取token ptr是一个 void * size是用户想要获取的token数
int mytbf_fetchtoken(mytbf_t *ptr,int size){
    struct mytbf_st *tbf = ptr;

    if (size <= 0){
        return -EINVAL;
    }
    
    //有token继续
    while (tbf->token <= 0)
      pause();
    
    int n =tbf->token<size?tbf->token:size;

    tbf->token -= n;
    //用户获取了 n 个token
    return n;
}

//归还token ptr是一个 void *
int mytbf_returntoken(mytbf_t *ptr,int size){
    struct mytbf_st *tbf = ptr;

    if (size <= 0){
        return -EINVAL;
    }
    
    tbf->token += size;
    if (tbf->token > tbf->burst)
        tbf->token = tbf->burst;

    return size;
}

int mytbf_destroy(mytbf_t *ptr){
    struct mytbf_st *tbf = ptr;
    job[tbf->pos] = NULL;
    free(tbf);
    return 0;
}
//main.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <math.h>

#include "mytbf.h"

static const int SIZE = 1024;
static const int CPS = 3;
static const int BURST = 100;//最大令牌数

static volatile int token = 0;//持有令牌数

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

    mytbf_t *tbf;

    tbf = mytbf_init(CPS,BURST);
    if (tbf == NULL){
        fprintf(stderr,"tbf init error");
        exit(1);
    }

    //打开文件
    int sfd,dfd = 0;
    do{
        sfd = open(argv[1],O_RDONLY);
        if (sfd < 0){
            if (errno == EINTR)
              continue;
            fprintf(stderr,"%s\n",strerror(errno));
            exit(1);
        }
    }while(sfd < 0);

    char buf[SIZE];
    
    while(1){
        
        int len,ret,pos = 0;
        int size = mytbf_fetchtoken(tbf,SIZE);
        
        int i = 0;
        while(i < 2){
            sleep(1);
            i++;
        }

        if (size < 0){
            fprintf(stderr,"mytbf_fetchtoken()%s\n",strerror(-size));
            exit(1);
        }

        len = read(sfd,buf,size);
        while (len < 0){
            if (errno == EINTR)
              continue;
            strerror(errno);
            break;
        }

        //读取结束
        if (len == 0){
            break;
        }

        //要是读到结尾没用完token
        if (size - len > 0){
            mytbf_returntoken(tbf,size-len);
        }

        //以防写入不足
        while(len > 0){
            ret = write(dfd,buf+pos,len);
            while (ret < 0){
                if (errno == EINTR){
                  continue;
                }
                printf("%s\n",strerror(errno));
                exit(1);
            }

            pos += ret;
            len -= ret;
        }
    }

    close(sfd);
    mytbf_destroy(tbf);

    exit(0);
}

9.4 setitimer函数

上面的计时等核心都是通过alarm函数实现的。实际开发过程中,
优先使用setitimer来计时!更灵活 而且误差不累积。(计时,优先使用setitimer)
(程序中优先使用setitimer,而不是alarm!!!)

参数:
1、which

  • TIMER_REAL 以系统真实的时间来计算,时间到了 它送出SIGALRM信号。可以代替 alarm

  • ITIMER_VIRTUAL 以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号

  • ITIMER_PROF 以该进程在用户态下和内核态下所费的时间来计算。它送出SIGPROF信号。

2、相关结构体

 struct itimerval {
           struct timeval it_interval; /* Interval for periodic timer */ 设置的定时器时间
           struct timeval it_value;    /* Time until next expiration */ 定时器剩余时间
       };

       struct timeval {
           time_t      tv_sec;         /* seconds */
           suseconds_t tv_usec;        /* microseconds */
       };

下面使用一个alarm或者setitimer计时器来实现多任务计时器的功能。

//流量控制之漏桶实验,用setitimer()实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/time.h>

#define BUFSIZE 10

static volatile int loop = 0;

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

int main(int argc,char *argv[])
{
	int sfd,dfd=1;
	char buf[BUFSIZE];
	int len,ret,pos;
	struct itimerval itv;
	if(argc < 2)
	{
		fprintf(stderr,"Usage:%s <src_file> <dest_file>\n",argv[0]);
		exit(1);
	}
	

	signal(SIGALRM,alrm_handler);
	//alarm(1);
	itv.it_interval.tv_sec = 1;
	itv.it_interval.tv_usec = 0;
	itv.it_value.tv_sec = 1;
	itv.it_value.tv_usec = 1;

	if(setitimer(ITIMER_REAL,&itv,NULL) < 0)
	{
		perror("setitimer()");
		exit(1);
	}

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

		}
	}while(sfd < 0);


	while(1)
	{

		while(!loop)
			pause();
		
		loop = 0;

		while((len = read(sfd,buf,BUFSIZE)) < 0)
		{	if(errno == EINTR)//signal
				continue;
			perror("read()");
			break;
		}
		if(len == 0)
			break;

		//确保写进去 len 个字节
		pos = 0;
		while(len > 0)
		{
			ret = write(dfd,buf+pos,len);
			if(ret < 0)
			{
				if(errno == EINTR) //signal
					continue;
				perror("write()");
				exit(1);
			}
			pos += ret;
			len -= ret;
		}
	}
	close(sfd);	
}

9.5 system

如果想在有信号的程序当中正常使用 system(),需要阻塞SIGCHLD信号,忽略SIGINT信号和SIGQUIT信号

10 信号集

sigemptyset()
sigfillset()
sigaddset()
sigdelset()
sifismember()

1、信号屏蔽字/pending集的处理
sigprocmask():我们无法控制信号何时到来,但可以选择如何响应它

例一:
每秒向标准输出打印一个* 打印五秒换行。程序不受 CTRL+C 即 SIGINT信号影响,收到 SIGINT信号 响应信号 打印!。

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

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

int main()
{
	int i,j;

	signal(SIGINT,sig_handler);
	for(j=0;j < 1000;j++)
	{

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


	exit(0);
}

例二:
修改: sigprocmask(SIG_BLOCK…)+ sigprocmask(SIG_UNBLOCK…)只在两行*之间输出 !, 即 选择响应 SIGINT 信号的时间。

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


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

int main()
{
	int i,j;
	sigset_t set;//创建信号集

	signal(SIGINT,sig_handler);
	
	sigemptyset(&set); //清空信号集
	sigaddset(&set,SIGINT); // 添加 SIGINT信号 到信号集


	for(j=0;j < 1000;j++)
	{

		sigprocmask(SIG_BLOCK,&set,NULL); // 对 set信号集中的信号 进行阻塞,即将mask信号屏蔽位置0
		for(i=0 ; i<5 ; i++) 
		{
			write(1,"*",1);
			sleep(1);
		}	
		write(1,"\n",1);
		sigprocmask(SIG_UNBLOCK,&set,NULL); // 对 set信号集中的信号 解除阻塞,即将mask信号屏蔽位置1
	}


	exit(0);
}

mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ gcc sigprocmask.c 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ ./a.out 
***^C*^C*^C^C^C^C^C
!*^C^C^C^C^C^C*^C^C^C^C^C^C*^C**
!*****
*^C*^C^C^C^C^C*^C^C*^C*
!*****
*****
*****
*****
*****
****^C*^C
!***^C^C**
!*****^\Quit (core dumped)
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 

发现实验中 SIGINT 信号不会打断阻塞的系统调用了。只有 SIG_BLOCK 之后才会,原因同上,因为已经屏蔽了该信号。

例三:
上面的例二有个问题,还是宏观思想的问题,当进入上面这个模块之后,若是SIGINT本身是非阻塞的,那么没有问题,因为这个模块先将信号阻塞然后接触阻塞。如果这个信号进入这个模块前是阻塞的,那么将会出问题,因为从这个模块出去后对SIGINT信号将是非阻塞的!
所以进入每个模块前要先记录资源的原始状态,从模块结束后恢复原状态!

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


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

int main()
{
	int i,j;
	sigset_t set,saveset;//创建信号集

	signal(SIGINT,sig_handler);
	
	sigemptyset(&set); //清空信号集
	sigaddset(&set,SIGINT); // 添加 SIGINT信号 到信号集

//在当前模块修改之前 保存信号集状态,以便 当前模块执行完成后恢复 信号集状态
sigprocmask(SIG_UNBLOCK,&set,&saveset);

	for(j=0;j < 1000;j++)
	{

		sigprocmask(SIG_BLOCK,&set,NULL); // 对 set信号集中的信号 进行阻塞,即将mask信号屏蔽位置0
		for(i=0 ; i<5 ; i++) 
		{
			write(1,"*",1);
			sleep(1);
		}	
		write(1,"\n",1);
		sigprocmask(SIG_UNBLOCK,&set,NULL); // 对 set信号集中的信号 解除阻塞,即将mask信号屏蔽位置1
	}

// 恢复 之前信号集状态
sigprocmask(SIG_SETMASK,&saveset,NULL);

	exit(0);
}

11 sigsuspend() 设置信号集状态与挂起 的原子操作

函数功能:
(以下三步为原子操作)

第一步:设置当前进程信号集状态为 目标信号集状态mask,并保存进程旧的信号集状态为 oldmask

第二步:马上进入等待信号阶段

第三步:收到信号唤醒后 设置当前进程信号集状态为 旧的信号集状态oldmask。

例子一:
用pause()实现,打印一行*后 ,停止等待信号 驱动程序继续跑,
pause()休眠 等待信号唤醒,CTRL+C 唤醒程序执行打印下一行。

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


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

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

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

	
	sigprocmask(SIG_UNBLOCK,&set,&saveset);

	for(j=0;j < 1000;j++)
	{

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

	sigprocmask(SIG_SETMASK,&saveset,NULL);

	exit(0);
}
mhr@ubuntu:~/Desktop/xitongbiancheng/test$ ./a.out 
*****
^C!*****
^C!**^C***
!

上面需要注意的:

  • crtl+\ :是终止进程。
  • crtl+c :也是终止进程。
  • crtl+z:是将进程挂起,并不是结束进程,可以再次输入fg,将挂起的进程再次运行!
  • pause,使调用进程(线程)进入休眠状态(就是挂起);直到接收到信号且信号处理函数成功返回 pause函数才会返回

上面的程序存在问题,如果在当前行打印途中 发送 SIGINT信号,不会驱动程序打印下一行数据,这是因为在sigprocmask(SIG_UNBLOCK,&set,NULL); pause()这两个函数中间,当解除信号阻塞之后,还没来得及执行pause(),就把 SIGINT信号响应掉了,所以信号就作用不到 pause(),即不会唤醒进程,程序响应 SIGINT信号之后,打印!再执行到 pause()。 我们知道,程序在运行当中 可以理解为 时时刻刻都在被中断,时间片一直在反复的耗尽,也就是反复 进入内核态等待调度,再继续执行程序,所以此时 刚刚好从内核态切换回用户态,发现SIGINT信号,于是马上响应信号动作。我们的初衷是 执行完解除阻塞后,马上pause()挂起,最后SIGINT信号作用到 pause()上,打印下一行 ,所以 这个问题的原因就是 sigprocmask(SIG_UNBLOCK,&set,NULL) 与 pause() 这两个操作组合 是非原子操作,是不原子的。

例二:
修改:用 sigsuspend()实现

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


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

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

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

	
	sigprocmask(SIG_UNBLOCK,&set,&saveset);//保存进入该模块前的状态

	sigprocmask(SIG_BLOCK,&set,&oset); 
	for(j=0;j < 1000;j++)
	{

		
		for(i=0 ; i<5 ; i++) 
		{
			write(1,"*",1);
			sleep(1);
		}	
		write(1,"\n",1);
		
		sigsuspend(&oset);
	}

	sigprocmask(SIG_SETMASK,&saveset,NULL);//恢复进入该模块前的状态

	exit(0);
}
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ gcc sigsuspend.c 
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ ./a.out 
*****
^C!*****
^C!**^C*^C^C**
!*****
^C!**^\Quit (core dumped)
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ 

12 sigaction(),区别于 signal()

  • signal()的缺陷1 :当多个信号共用一个信号处理函数的时候,可能会发生重入,导致段错误。所以我们希望,在响应一个信号的时候,将其他信号阻塞。 sigaction()可以弥补

  • signal()的缺陷2 :不能指定接收信号来源, sigaction()可以弥补.

signal()的缺陷1 试验:当多个信号共用一个信号处理函数的时候,可能会发生重入。以前面的守护进程为例:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <syslog.h>

#define FILENAME "/tmp/out"

static int craetdeamon(void)
{
	pid_t pid;
	int fd;	
	
	pid = fork();
	if(pid < 0)
	{
		perror("fork()");
		return -1;
	}

	if(pid > 0)
	{
		printf("%d\n",getpid());
		exit(0);
	}

	fd = open("/dev/null",O_RDWR);
	if(fd < 0)
	{	
		perror("open()");
		return -1;
	}

	dup2(fd,0);
	dup2(fd,1);
	dup2(fd,2);	
	if(fd > 2)
	{
		close(fd);
	}
	setsid();return

	chdir("/");
	return 0;
}

static void exitdeamon(int s)
{
	fclose(fb);
	closelog();
}


int main(int argc,char* argv[])
{
	FILE* fp;
	int i;

signal(SIGINT,exitdeamon);
signal(SIGQUIT,exitdeamon);
signal(SIGTERM,exitdeamon);

	openlog("craetdeamon",LOG_PID,LOG_DAEMON);
	if(craetdeamon())
	{
		syslog(LOG_ERR,"craetdeamon failed!");
		exit(1);
	}else{
		syslog(LOG_INFO,"craetdeamon successded!");
	}

	fp = fopen(FILENAME,"w");
	if(fp == NULL)
	{
		syslog(LOG_ERR,"fopen %s failed!",FILENAME);
		exit(1);
	}

	syslog(LOG_INFO,"fopen %s successede!",FILENAME);

	for(i = 0; ;i++)
	{
		fprintf(fp,"%d\n",i);
		fflush(fp);
		syslog(LOG_DEBUG,"%d is printed!",i);
		sleep(1);
	}
	exit(0);

}

程序的本意是,守护进程 接收到 SIGINT,SIGQUIT,SIGTERM 三个信号任意信号 的时候终止守护进程,多个信号共用一个信号处理函数。但是有一个问题:
-接收到这三个信号中的任意信号,都会执行处理函数中内容,假如有这样的情形:

  • 1 程序接受到了 SIGINT 信号,程序收到中断后扎内核,被调度后,从内核态切换到用户态,发现 收到了 SIGINT信号,于是开始执行处理函数,但是只执行了 fclose(fb); 这一句,就再次被打断,进程内核态,等待调度。

  • 2 在等待调度的时候,程序又收到了 SIGQUIT 信号,等到程序被调度,从内核态切换到用户态时候,发现收到了 SIGQUIT信号,于是再次 执行 信号处理函数,再次执行到 fclose(fb);。于是fb 被两次 fclose(),会发生段错误。发生了重入!!

用 sigaction() 实现多信号共用一个处理函数。

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

#define FILENAME "/tmp/out"

static FILE* fp;

static int craetdeamon(void)
{
	pid_t pid;
	int fd;	

	pid = fork();
	if(pid < 0)
	{
		perror("fork()");
		return -1;
	}

	if(pid > 0)
	{
		printf("%d\n",getpid());
		exit(0);
	}

	fd = open("/dev/null",O_RDWR);
	if(fd < 0)
	{	
		perror("open()");
		return -1;
	}

	dup2(fd,0);
	dup2(fd,1);
	dup2(fd,2);	
	if(fd > 2)
	{
		close(fd);
	}
	setsid();return
	chdir("/");
	return 0;

}

static void exitdeamon(int s)
{
	fclose(fp);
	closelog();
}



int main(int argc,char* argv[])
{
	int i;
	struct sigaction sa;


	sa.sa_handler = exitdeamon;
	sigemptyset(&sa.sa_mask);
	sigaddset(&sa.sa_mask,SIGQUIT);
	sigaddset(&sa.sa_mask,SIGTERM);
	sigaddset(&sa.sa_mask,SIGINT);

	sigaction(SIGINT,&sa,NULL);
	sigaction(SIGTERM,&sa,NULL);
	sigaction(SIGQUIT,&sa,NULL);

	openlog("craetdeamon",LOG_PID,LOG_DAEMON);

	if(craetdeamon())
	{
		syslog(LOG_ERR,"craetdeamon failed!");
		exit(1);
	}else{
		syslog(LOG_INFO,"craetdeamon successded!");
	}


	fp = fopen(FILENAME,"w");
	if(fp == NULL)
	{
		syslog(LOG_ERR,"fopen %s failed!",FILENAME);
		exit(1);
	}

	syslog(LOG_INFO,"fopen %s successede!",FILENAME);

	for(i = 0; ;i++)
	{
	fprintf(fp,"%d\n",i);
		fflush(fp);
		syslog(LOG_DEBUG,"%d is printed!",i);
		sleep(1);
	}

	exit(0);

}

signal()缺陷2:不能识别信号来源

回顾之前的流量控制(流量控制的库):
在终端1 运行:

mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ ./a.out /etc/services

在终端2 运行

mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/signal/test$ while true ; do kill -ALRM 10405 ;done

会发现 程序会瞬间执行完成。即从其他终端以用户的角度向指定进程不停的发送 ALRM 信号,导致流控失效。问题在于 signal() 并不会检查区分信号的来源,属性信息,只要来了该信号,就会响应动作。但是实际上程序利用 alarm来发送信号,实际上 alarm信号是从 kernel 发送过来的,而刚刚的实验信号,是从user 发送的。所以需要指定只响应从某处来的信号,即指定信号来源,指定从 kernel来的信号,signal() 无法完成该动作,sigaction()可以。

利用sigactino修改后的版本:

//信号处理函数
static void handler(int sig,siginfo_t *infop,void *unused){
    struct itimerval itv;

    if (infop->si_code != SI_KERNEL){
        return ;
    }

    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);
    }
    for (int i = 0;i < MYTBF_MAX;i++){
        if (job[i] != NULL){
            job[i]->token += job[i]->csp;
            if (job[i]->token > job[i]->burst){
                job[i]->token = job[i]->burst;
            }
        }
    }
}

//装载信号处理模块
static void mod_load(){
    struct sigaction sa;
    sa.sa_sigaction = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;

    if (sigaction(SIGALRM,&sa,&old_sa) < 0){
        perror("sigaction()");
        exit(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,&old_itv) < 0){
        perror("setitimer()");
        exit(1);
    }
}
//卸载信号处理模块 当发生异常退出时 可以将占用的资源释放 将alarm信号取消
static void mod_unload(){
   //signal(SIGALRM,alarm_status);
   sigaction(SIGALRM,&old_sa,NULL);
   
   //恢复时钟信号状态
    struct itimerval itv;
    itv.it_interval.tv_sec = 0;
    itv.it_interval.tv_usec = 0;
    itv.it_value.tv_sec = 0;
    itv.it_value.tv_usec = 0;
    if(setitimer(ITIMER_REAL,&itv,&old_itv) < 0){
        perror("setitimer()");
        exit(1);
    }

    for (int i = 0;i < MYTBF_MAX;i++){
        free(job[i]);
    }
}

13 实时信号

如果一个进程既受到标准信号,又受到实时信号,那么先响应标准信号后相应实时信号。

标准信号特点:

  • 1 标准信号会丢失
  • 2 标准信号 没有一个严格的响应顺序要求,未定义行为。

标准信号中有两个未定义行为的信号,留给我们使用:SIGUSR1,SIGUSR2

实时信号用于解决标准信号的不足:

  • 1 实时信号不会丢失
  • 2 实时信号响应有顺序要求

实时信号的例子:
选择40) SIGRTMIN+6 信号举例

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

#define MYRTSIGNAL (SIGRTMIN+6)

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

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

	signal(MYRTSIGNAL,sig_handler);
	
	sigemptyset(&set); 
	sigaddset(&set,MYRTSIGNAL); 

	
	sigprocmask(SIG_UNBLOCK,&set,&saveset);

	sigprocmask(SIG_BLOCK,&set,&oset); 
	for(j=0;j < 1000;j++)
	{

		
		for(i=0 ; i<5 ; i++) 
		{
			write(1,"*",1);
			sleep(1);
		}	
		write(1,"\n",1);
		
		sigsuspend(&oset);
	}

	sigprocmask(SIG_SETMASK,&saveset,NULL);

	exit(0);
}

LINUX系统编程-- 5 信号

发现实时信号不会丢失,连续发送三次信号,如果是标准信号,只会响应一次,而实时信号响应了三次。

上一篇:ADB命令与Dumpsys alarm查看应用程序唤醒命令


下一篇:实验七信号