1、信号注册函数: signal
#include
void (*signal(int signum, void (*sighandler_t)(int))) (int); typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
入参:
signum 哪个信号
handle 信号所对应的处理函数;SIG_IGN:忽略此信号;SIG_DFL:按系统默认
方式处理
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
2、修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功:0;失败:-1,设置errno
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
struct sigaction结构体
struct sigaction {
void
void
sigset_t
int
void
(*sa_handler)(int);
(*sa_sigaction)(int, siginfo_t *, void *);
sa_mask;
sa_flags;
(*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。
内核实现信号捕捉过程:
信号捕捉特性:
1、进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
2、XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
3、阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排
队)
3、pause函数 : 挂起当前的进程,直到收到一个信号,才会接着执行
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
int pause(void);
返回值:-1 并设置errno为EINTR
① 如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。 ② 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。 ③ 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】errno设置为 EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回
值。
④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
4、alarm函数
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒数,无失败。
常用:取消定时器alarm(0),返回旧闹钟余下秒数。
例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。
使用time命令查看程序执行的时间。程序运行的瓶颈在于IO,优化程序,首选优化IO。
实际执行时间 = 系统时间 + 用户时间 + 等待时间
5、raise函数: 自己给自己发送信号 raise(signo) == kill(getpid(), signo);
int raise(int sig);
成功:0,失败非0值
入参: 发送的信号
6、abort 函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件 void abort(void); //该函数无返回
kill函数/命令产生信号
1、kill命令产生信号:kill -SIGKILL pid
2、kill函数:给指定进程发送指定信号(不一定杀死)
7、int kill(pid_t pid, int sig);
成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
pid > 0: 发送信号给指定的进程。
pid = = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程。
pid < 0: 取|pid|发给对应进程组。
pid = = -1:发送给进程有权限发送的系统中所有进程。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID
相同。
权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root用户的pid) 是不可以的。同样,普通用户也不能向其他
普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID
时序竞态
设想如下场景:
欲睡觉,定闹钟10分钟,希望10分钟后闹铃将自己唤醒。
正常:定时,睡觉,10分钟后被闹钟唤醒。
异常:闹钟定好后,被唤走,外出劳动,20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过,不会再将我唤醒。
时序问题分析
回顾,借助pause和alarm实现的mysleep函数。设想如下时序:
1. 注册SIGALRM信号处理函数 (sigaction...)
2. 调用alarm(1) 函数设定闹钟1秒。
3. 函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
4. 1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无
关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
5. 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。
SIGALRM信号递达,信号设置捕捉,执行处理函数sig_alarm。
6. 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲
等待alarm函数发送的SIGALRM信号将自己唤醒)
7. SIGALRM信号已经处理完毕,pause不会等到。
解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场
合下都应该使用sigsuspend替换pause。
int sigsuspend(const sigset_t *mask);//挂起等待信号。
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
8、setitimer函数
设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
成功:0;失败:-1,设置errno
参数:which:指定定时方式
① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间
② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占
用cpu的时间
③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF 计算占用cpu及执行系
统调用的时间
信号集操作函数:
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。
信号集设定:
sigset_t set; // typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); //将某个信号集清0
成功:0;失败:-1
int sigfillset(sigset_t *set); //将某个信号集置
1
成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); //将某个信号加入信号集
成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); //将某个信号清出信号集
成功:0;失败:-1
int sigismember(const sigset_t *set, int signum);//判断某个信号是否在信号
集中
返回值:在集合:1;不在:0;出错:-1
sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证
跨系统操作有效。
对比认知select 函数。
9、sigprocmask函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB
中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号
丢处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失败:-1,设置errno
参数:
set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回
前,至少将其中一个信号递达。
10、sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t *set); //set传出参数。
返回值:成功:0;失败:-1,设置errno