信号的机制:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似–异步模式。但信号是软件层面上实现的中断,早期被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
与信号相关的时间和状态产生信号:
1、 按键产生
2、 系统调用产生,如kill,raise, aborrt。
3、 软件条件产生,如定时器。
4、 硬件异常产生, 如非法访问内存(段错误), 除0(浮点数例外),内存对齐出错(总线错误)
5、 命令产生,如:kill命令
递达:递送并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式:
1、 执行默认动作
2、 忽略(丢弃)
3、 捕捉(调用户处理函数)
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集:将某些信号加入集合,对他们设置屏蔽,当屏蔽X信号后,再收到该信号,该信号的处理将推后。
未决信号集:1、 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转为0.这一时刻非常短暂。2、 信号产生后由于某些原因(阻塞)不能抵达。这类信号的集合为未决信号集。在屏蔽解除前,信号一直处于未决状态。
阻塞信号集和未决信号集本质都是位图。
信号的编号:使用kill -l命令查看当前系统可使用的信号有哪些。不存在编号为0的信号。其中1-31号信号称之为常规信号,34-64称之为实时信号。驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。
信号四要素信号使用前必须先确定信号四要素
1、 编号 2、 名称 3、 事件 4、 默认处理动作 可通过man 7 signal 查看帮助文档。
kill函数产生信号kill命令产生信号:kill -SIGKILL pid ps ajx
kill函数:给指定进程发送指定信号。 传复数杀死组内所有进程。
pid > 0 : 发送信号给指定的进程。
pid == 0 : 发送信号给与调用kill函数进程属于同一进程组的所有进程。
pid < 0 : 取|pid|发给对应进程组。
pid = -1 : 发送给进程有权限发送的系统中的所有进程。能发的全部可以发送,全杀。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
权限保护:super用户可以发送信号给任意用户,普通用户是不能向 系统用户发送信号的。kill -9 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向 自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID==接收者实际或有效用户ID。
练习:循环创建5个子进程,父进程用kill函数终止任意子进程。
alarm函数设置定时器,在指定seconds后,内核会给当前进程发送14)SIGALRM 信号。进程收到该信号,默认动作终止。 每个进程有且只有唯一一个定时器。
unsigned int alarm(unsigned int seconds); 返回0或剩余的秒数,无失败。
定时与进程状态无关,就绪、运行、挂起、终止、僵尸,无论进程处于何种状态,alarm都计时。
编写程序,测试使用的计算机1秒钟能数多少个数。
使用time命令查看程序执行的时间。程序运行的瓶颈在于IO,首选优化IO。
实际执行时间 = 系统时间 + 用户时间 + 等待时间
setitimer函数设置定时器。可代替alarm函数。精度微妙us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 成功:0;失败:-1,设置errno。
参数:which 指定定时方式
1.自然定时: ITIMER_REAL -> 14)SIGLATM 计算自然时间。
2.虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26)SIGVTALRM 只计算进程占用cpu的时间
3.运行时计时(用户+内核): ITIMER_PROF -> 27)SIGPROF 计算占用cpu及执行系统调用的时间。
其余发信号函数: int raise(int sig); void abort(void);
new_value:定时秒数
类型:
struct itimerval {
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_interval;---> 周期定时秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次定时秒数
};
old_value:传出参数,上次定时剩余时间。
e.g.
struct itimerval new_t;
struct itimerval old_t;
new_t.it_interval.tv_sec = 0;
new_t.it_interval.tv_usec = 0;
new_t.it_value.tv_sec = 1;
new_t.it_value.tv_usec = 0;
int ret = setitimer(&new_t, &old_t); 定时 1 秒
返回值:
成功: 0
失败: -1 errno
setitimer()函数 alarm函数会建立一个定时器,在seconds秒后,给当前进程发送SIGALRM信号 setitimer也可以创建定时器,和alarm函数相比,最显著的区别就是它可以指定到微秒,而且可以循环发送 man 2 setitimer #include信号集操作函数#include #include #include #include void sigHandler(int sig){ printf("[%d] caught signal : #%d: %sn",time(NULL),sig,strsignal(sig)); } int main() { //打印开始的时间戳 printf("[%d] startn",time(NULL)); //注册SIGALRM信号处理函数,setitimer函数会不停的发送SIGALRM 信号 signal(SIGALRM,sigHandler); struct itimerval it,oldit; //第三秒时发送一次SIGALRM 信号 it.it_interval.tv_sec = 3; it.it_interval.tv_usec = 0; //以后每隔一秒发送一次SIGALRM信号 it.it_value.tv_sec = 1; it.it_value.tv_usec = 0; setitimer(ITIMER_REAL,&it,NULL); while(1); return 0; }
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应用程序中自定义 set 来改变 mask。已达到屏蔽指定信号的目的。
信号集操作函数:
sigset_t set; 自定义信号集。 sigemptyset(sigset_t *set); 清空信号集 sigfillset(sigset_t *set); 全部置 1 sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中 sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除 sigismember( const sigset_t *set, int signum); 判断一个信号是否在集合中。 在: 1, 不在: 0,出错: -1;
sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
设置信号屏蔽字和解除屏蔽:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义 set 替换 mask。
set: 自定义 set
oldset:旧有的 mask。
查看未决信号集:
int sigpending(sigset_t *set);
set: 传出的 未决信号集。
sigprocmask函数用来屏蔽信号、解除屏蔽。其本质是读取或修改进程的信号屏蔽字。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigpending函数读取当前进程的未决信号集
int sigpending(sigset_t *set);set传出参数
kill -l可以显示信号列表
设置信号阻塞
#include#include #include #include #include #include #include void sys_err(const char *str){ perror(str); exit(1); } void print_set(sigset_t *set){ int i; for(i = 1; i < 32; i++){ if(sigismember(set, i)) putchar('1'); else putchar('0'); } printf("n"); } int main(){ sigset_t set, oldset, pedset; //定义一个信号集 int ret = 0; sigemptyset(&set);//清空信号集 sigaddset(&set, SIGINT);//设置2号信号为1 ret = sigprocmask(SIG_BLOCK, &set, &oldset); // 将自定义的信号集或上未决信号集,只有未决信号集可以操作 if(ret == -1){ sys_err("sigprocmask error"); } while(1){ ret = sigpending(&pedset); //查看未决信号集 if(ret == -1){ sys_err("sigpending error"); } print_set(&pedset); sleep(1); } return 0; }
运行结果为:
ymyy@ymyy-virtual-machine:~/systemcode$ ./sigsfunc 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 0000000000000000000000000000000 ^C0100000000000000000000000000000 0100000000000000000000000000000 0100000000000000000000000000000 0100000000000000000000000000000 0100000000000000000000000000000 0100000000000000000000000000000 2号信号被阻塞,即 ctrl+c 的信号被阻塞, 9号和19号信号不能忽略捕捉,不能设置阻塞。
设置信号捕捉
signum: 带捕捉信号
handler: 捕捉信号后的操纵函数
signal 函数: 注册一个信号捕捉函数:
typedef void(*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 该函数由ANSI定义,由于历史原因在不同版本的unix和不同版本的linux中可能由不同的行为。因此尽量使用 sigaction 函数 void(*signal(int signum, void(*sighandler_t)(int)))(int); #include#include #include #include #include void sig_catch(int signal){ printf("catch you!! %dn", signal); return; } int main(){ signal(SIGINT, sig_catch); while(1); return 0; }
sigaction函数
修改信号处理动作(注册一个信号捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
#include#include #include #include #include void sig_catch(int signal){//回调函数,内核帮忙调用 printf("catch you!! %dn", signal); return; } int main(){ struct sigaction act, oldact; act.sa_handler = sig_catch;//设置捕捉函数 sigemptyset(&(act.sa_mask));//清空屏蔽字,只在sig_catch工作时有效 act.sa_flags = 0;//设置默认属性 sigaction(SIGINT, &act, &oldact);//注册信号捕捉函数 while(1); return 0; }
信号捕捉函数的特性:
1.进程正常运行时,默认PCB中有一个信号屏蔽字,它决定了进程自动屏蔽那些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在此期间会再次收到该信号从而会陷入死循环,因此设置了sa_mask,在函数执行期间,sa_mask会清空屏蔽字,屏蔽本信号,保证信号捕捉函数不会陷入死循环。
2.阻塞的常规信号不支持排队,多次的信号内核只处理一次。
内核实现信号的捕捉解析:sys_sigreturn() 只干一件事,执行流程从用户空间切换到内核空间,内核空间发挥用户模式,从主控制流程中上次被中断的地方继续向下执行。
SIGCHLD信号产生条件:
子进程终止时
子进程接收到 SIGSTOP 信号时。
子进程处在停止态,接收 SIGCONT 后唤醒
借助 SIGCHLD 信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程的回收。
#include#include #include #include #include #include #include void catch_child(int signo){ pid_t wpid; int status; // while(wpid = wait(NULL) != -1) while((wpid = waitpid(-1, &status, 0)) != -1){ if (WIFEXITED(status)) printf("catch child id %d, return = %dn", wpid, WEXITSTATUS(status)); } return; } int main(int argc, char *argv[]){ pid_t pid; //阻塞 防止父进程还未注册成功子进程就结束 sigset_t set; sigemptyset(&set); sigaddset(&set, SIGCHLD); sigprocmask(SIG_BLOCK, &set, NULL); int i; for(i = 0; i < 5; i++){ if((pid = fork()) == 0){ break; } } //接触阻塞 sigprocmask(SIG_UNBLOCK, &set, NULL); if(i == 5){ struct sigaction act; act.sa_handler = catch_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); printf("i am parent, pid = %dn", getpid()); while(1); }else{ printf("i am child, pid = %dn", getpid()); return i; } return 0; }
运行结果为:
ymyy@ymyy-virtual-machine:~/systemcode$ ./catch_child i am child, pid = 115630 i am child, pid = 115631 i am child, pid = 115632 i am child, pid = 115633 catch child id 115630, return = 0 catch child id 115631, return = 1 i am child, pid = 115634 catch child id 115632, return = 2 catch child id 115633, return = 3 catch child id 115634, return = 4 i am parent, pid = 115629中断系统调用
系统调用可分为两类:慢速系统调用和其它系统调用
慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行;也可以设定系统调用是否重启。如,read,write,pause,wait…
其它系统调用: getpid, getppid, frok
慢速系统调用被中断的相关行为,实际上就是pause的行为:如read
1、想中断 pause,信号不能被屏蔽
2、信号的处理方式必须是捕捉
3、中断后返回-1,设置errno为EINTR,表示信号被中断
可修改sa_flags 参数来设置被信号中断后系统调用是否重启。SA_INTERRURT 不重启。SA_REATART重启



