title: linux/unix编程手册-21_25 date: 2018-06-16 11:53:07 categories: programming tags: tips
linux/unix编程手册-21(信号处理器函数)
信号处理器函数设计
- 信号处理器函数设置全局标志变量并退出,主程序对此标记进行周期检查,一旦置位随即采取相应动作
- 信号处理器函数执行某种类型的清理,之后终止进程或者使用非本地跳转将栈解开并将控制返回主程序定位置
- 可重入:
- 同一进程多线程可以同时安全的调用一个函数(信号处理函数可在任意时间点中断程序的执行,从而在一个进程中形成2条独立但不是并发的执行线程)
- 异步信号安全:
- 如果一个函数是可重入的或者信号处理函数无法将其中断
- 仅当信号处理函数中断了不安全函数的执行,且处理函数自身也调用了这个函数时,该函数才是不安全的
实现的两种策略
- 确保信号处理器函数代码本身是可重入的,且只调用异步信号安全的函数
- 当主程序执行不安全函数或者是去处理信号处理器函数可能更新的全局数据结构时,阻塞信号的传递
全局变量和sig_atomic_t
volatile避免将全局变量优化的寄存器sig_atomic_t保证读写原子性- 主程序和信号处理器函数共享的全局变量声明如下
volatile sig_atomic flag
信号处理器函数终止的方法
- 调用_exit(),不是exit()(会清除I/O缓冲不安全)
- 调用kill()来杀掉进程
- 信号处理器函数执行非本地跳转:
- 略
- 调用abort()终止进程并产生核心转储
- 略
系统调用的中断和重启
信号处理器函数中断了阻塞的系统调用时,系统调用会产生EINTR报错
linux/unix编程手册-22(信号的高级特性)
核心转储文件
- 内含进程终止时内存镜像的一个文件(默认进程工作目录/core)
- 文件命名/proc/sys/kernel/core_pattern
信号特殊情况
SIGKILL和SIGSTOP的默认行为无法更改,调用signal()和sigaction()来改变时总会返回错误;同时也不能阻塞SIGCONT如果一个进程处于停止状态,SIGCONT总会使其回复;进程收到SIGCONT会将等待状态的停止信号丢弃,反之收到停止信号,会将等待状态的SIGCONT丢弃
可中断不可中断进程的休眠状态
内核经常需要领进程休眠,休眠有两种
- TASK_INTERRUPTIBLE:等待某一事件(终端输入等等),传递进来的信号会唤醒进程 ps STAT :S
- TASK_UNINTERRUPTIBLE:等待特定事件(磁盘I/O完成等的),在摆脱状态前,系统不会把信号传递给进程 ps STAT:D
- 一般这种状态转瞬即逝,但是如果因为硬件故障等问题 因为(
SIGKILL,SIGSTOP)不会终止挂起进程,只能通过重启- TASK_KILLABLE:类似于前者,但是收到杀死进程信号会唤起
信号传递的时机与顺序
- 同步信号(进程自己产生的信号,自己调用kill(), raise()等等)会立即传递
- 异步信号在进程发生内核态到用户态的下一次切换时
- 进程在前度超时后,再度获得调用
- 系统调用完成
如果通过
sigprocmask()解除了多个等待信号的阻塞。这些信号会立刻传给进程
- Linux下通常是根据信号编号升序传递
- 同时如果调度器函数引起了内核态和用户态的切换,会中断函数转而调用下一个信号处理器函数
实时信号
- 信号范围扩大
- 队列化管理,如果将某一实时信号的多个实例发给一进程,信号会多次传递
- 可为信号指定伴随数据
- 传递顺序得到保障,等待恢复后(信号编号小加时间早先传递)
#define _POSX_C_SOURCE 199309
#include<signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// 权限和kill()一致,但是pid不能为负
union sigval{
int sival_int;
void *sival_ptr;
}
使用掩码等待信号(原子操作)
#include<signal.h>
int sigsuspend(const sigset_t *mask);
//sigsuspend(&mask) 等同于
sigprocmask(SIG_SETMASK, &mask, &prevMask);
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);
调用,捕获,文件描述符获取信号略
信号用作IPC
限制比较多
- 信号异步的本质,可重入性,竟态条件,全局变量的设置
- 标准信号无法排队,实时信号排队数量限制
- 携带信息有限
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <unistd.h>
void sig_handler(int signum)
{
printf("in handler\n");
sleep(1);
printf("handler return\n");
}
int main(int argc, char **argv)
{
char buf[100];
int ret;
struct sigaction action, old_action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 版本1:不设置SA_RESTART属性
* 版本2:设置SA_RESTART属性 */
//action.sa_flags |= SA_RESTART;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
bzero(buf, 100);
ret = read(0, buf, 100);
if (ret == -1) {
perror("read");
}
printf("read %d bytes:\n", ret);
printf("%s\n", buf);
return 0;
}
// ctr+c 之后一个会ret=-1 一个会重新执行
linux/unix编程手册-23(定时器和休眠)
间隔定时器
#include<sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
// 0 success, -1 on error
int getitimer(int which, struct itimerval *curr_value);
// curr_value值和old_value值一致
struct itimerval{
struct timeval it_interval;
struct timeval it_value;
};
struct timeval{
time_t tv_sec;
suseconds_t tv_usec;
}
which取值
- ITIMER_REAL:真实时间倒计时的定时器,到期产生SIGALARM信号发送给进程
- ITIMER_VIRTUAL:用户模式下CPU时间倒计时计时器,到期产生SIGVTALRM
- ITIMER_PROF:进程时间(用户态和内核态CPU时间总和)的倒计时器,到期产生SIGPROF
new_value取值
- it_value 指定了延迟时间
- it_interval如果两个字段为0,则为一次性定时器,否则,在每次定时器到期后,会重置定时器指定间隔后再到期
- 如果调用setitimer时new_value 的 it_value的字段都为0,则屏蔽已有定时器
SUSv4废弃了
getitimer()和setitimer()
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
// 一次性定时器,返回剩余时间,alarm(0)屏蔽所有定时器,根据操作系统实现决定是不是和setitimer共享屏蔽
休眠
- 可通过定时器函数结合
sigsuspend实现sleep
include<unistd.h>
unsigned int sleep(unsigned int seconds);
//正常结束返回0,如果被终止返回剩余秒数
POSIX时钟,通常建议优先使用POSIX api
略
linux/unix编程手册-24(进程的创建)
- 系统调用
fork():子进程获取父进程的栈,数据段,堆和执行文本段的拷贝,同一程序文本段- 库函数
exit(status)(时系统调用_exit(status)的外层),将进程所占用的所有资源归还内核;父进程通过wait()获取该状态- 系统调用
wait(&status)
- 如果子进程未通过
exit()终止,挂起父进程直至子进程终止- 子进程状态通过
status参数返回- 父子进程一般只有一个会
exit()退出,另一个使用_exit()退出- 系统调用
execve(pathname, argv, envp)加载一个新程序到当前内存,丢弃现存的程序文本段,并重新创建栈,数据段以及堆
fork()
完成
fork()的调用后两个进程均会从fork()处返回,父进程返回子进程的pid,子进程返回0,无法创建时返回-1(可能是进程数超过了real_user_id的进程数上限或者系统级上限)
pid_t childPid;
switch (childPid = fork()){
case -1:
/*...*/
case 0:
/* child perform */
default:
/* parent perform */
}
父子进程文件共享
fork()文件描述符的创建类似dup(),指向相同的打开文件句柄,即文件偏移量等均是共享的,父子进程不会覆盖彼此的输出,但会乱序;shell中不加&父进程会等待子进程结束
fork()内存语义
- 原意是对程序段,数据段,堆段和栈段创建拷贝
- 会造成浪费,比如
fork()之后的exec()重新初始化....- 优化的措施
- 内核将每一个进程的代码段标记成RO,fork()为子进程的构建代码段时,其所构建的一系列进程级页表均指向父进程相同那个的物理内存页帧
- 数据段,堆段,栈段中的各页,内核采用写时复制(copy-on-write)之前(redis BUG有遇到过):将这些段的页表指向父进程物理地址相同的物理内存页,并将这些页标记成只读,之后为要修改的页建立copy
vfork()
- 尽量避免使用
- 适用于为子进程立刻调用
exec()而设计- 无需为子进程复制虚拟内存页活页表,共享父进程内存,直到成功调用exec()或_exit(),(文件描述符表每个进程是独立的)
- 在子进程调用exec()和_exit()之前暂停父进程
- 能保证调用
vfork()之后子进程先于父进程获得CPU调度
fork()之后的竞态条件
/proc/sys/kernel/sched_child_runs_first为0 则fork()之后父进程先调度
linux/unix编程手册-25(进程的终止)
进程终止的方式
- 通过信号终止进程,可能产生核心转储。
- 通过调用
_exit()正常终止,main()函数return n等同于exit(n)
- 如果在推出的处理过程中所执行的任何步骤需要访问
main()本地变量,那么从main()返回会导致未定义的行为(ex:setbuff()调用本地变量)
#include<unistd.h>
void _exit(int status);
//调用永远成功
库函数exit()
#include<stdlib.h>
void exit(in status);
// 不是异步信号安全函数
- 调用退出处理程序(通过
atexit()和on_exit()注册的函数),执行顺序与注册顺序相反- 刷新
stdio流缓冲区- 使用
status执行_exit()
进程终止的细节
- 关闭所有的打开文件描述符,目录流,信息目录描述符?以及转换描述符?
- 之后章节涉及的细节待补充。
退出处理程序
#include<stdlib.h>
int atexit(void (*func) (void));
// 0 on success
#define _BSD_SOURCE
#include<stdlib.h>
int on_exit(void (*func)(int, void *), void *args);
// 和 atexit是在同一列表注册,一样的执行和注册顺序相反
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("Hello\n");
write("STDOUT_FILENO", "WORLD\n",6);
if(fork() == -1){
exit(-1);
}
exit(EXIT_SUCCESS);
}
$ gcc fork_io.c -o fork_io
$ ./fork_io
Hello
WORLD
$ ./fork_io > 1.txt | cat 1.txt
WORLD
Hello
Hello
//终端为行缓冲,重定向到文件为块缓冲
避免方法
fork()之前fflush()刷新缓冲区,或调整stdio的缓冲选择- 确认的情况下子进程调用_exit()(应该不是很合适)