函数与 thread 结合起来有几个概念,还比较绕,这里总结一下。一个函数被称为
thread-safe:总是给出正确运行结果即使是从多个同步线程中反复调用
reentrant:不使用任何共享数据
需要注意的是,reentrant function 一定是 thread-safe 的,因为完全没有共享数据。而反 之则不然,例如一个 thread-safe 的函数可以使用同步机制来同步对于共享数据的访问。
另外,调用 thread-unsafe 函数的函数不一定就是 thread-unsafe 的。例如,如果只是被调 用的函数有一些共享数据,则完全可以通过同步来使其安全。
这里再给出 race 的定义
race:程序的执行结果的正确性取决于线程是如何调度的
thread 相关的函数
pthread_create(tid, attr, func, arg): 创建线程。tid 用于返回线程号,func 是要
运行的线程逻辑函数,arg 是一个指针,指向要传递的函数参数。成功返回 0
pthread_exit(rc): 结束当前线程
pthread_cancel(tid): 结束线程 tid
pthread_join(tid, ret): tid 是要 join 的线程号,ret 用于接收线程返回值,实际是
一个指针,因此可以用于实现多值返回
pthread_detach(tid): 将线程 tid detach,会使线程在结束时自动被 reap 而无需 join
。无须返回值的线程可以在线程开始时设
进程之间通过信号来进行同步中有很多小的细节容易被忽略。比如
1. 信号有可能在安装 signal handler 之前被收到和处理,这时会使用默认 handler
2. 信号有可能在程序结束之后才被收到和处理,也就完全不会被处理了
3. fork 和 exec 之后,信号的 block 状态是不会改变的
4. 信号的收发应该在多核并行场景下考虑,而不是仅在单核并发场景下
5. pause() 可以用来等待信号,但信号完全有可能在 pause() 之前收到,而这有可能导
致程序挂起不会退出。
6. signal handler 完全有可能被其它类型的信号打断。因此在 signal handler 中正式
开始处理时,要先保存 errno 和 block 所有其它信号,并在处理完后恢复这两者。
需要注意的是这里的 signal 指的是系统进程间用于同步的软信号,会导致相应的软件 signal hanlder 的触发。这一般发生在内核态向用户态切换时,如系统调用完成或者 context switch 发生。而硬件本身还有一类硬信号,一般是由 IO 设备产生,直接设置 CPU 的 中断线为高电平,再通过系统总线传入中断号。CPU 会在被每条指令结束之后检查中断线, 如果电平高位,就会根据相应的中断号调用系统中的 interrupt handler 处理。而这与进程 的软信号没有任何关系。其实某些情况下的 context switch 就是通过这种方式接收定时中 断实现的。
在编写 signal handler 时有以下几点要注意
1. 逻辑尽量简单。比如 handler 只设置标志,然后由 main 来定期处理
2. 仅调用 async-singal-safe 函数。特别地,printf, sprintf, malloc, exit 都不在
此类。唯一安全的产生输出的方式是使用 write
3. 保存和恢复 errno
4. 当访问共享数据时,通过 block 所有信号来尽量保证数据一致性
5. 将全局变量声明为 volatile。强制对其的每次读写都要通过内存访问进行,以免编
译器对其进行寄存器缓存
6. 将全局 flag 声明为 sig_atomic_t,这可以使对其的读写变为原子操作。但需要注意
的是即使进行了此声明,像 flag++/flag += 10 这种操作仍会是非原子操作,因为
其本身就涉及到多条指令
signal 相关的函数有以下几个
waitpid(pid, statusp, options): 等待子进程 pid 结束。当 pid 为 0 时,等待任何 pid
结束。当没有子进程需要被等待时,返回 -1 和 errno = ECHILD。当 waitpid 被信号
中断时,返回 -1 和 errno = EINTR。成功时,返回结束子进程的 pid。
statusp 用于返回子进程相关的结束信息
options 可以设置 waitpid 的等待行为
WNOHANG:立即返回,不挂起进程
WUNTRACED: 等待子进程直到某个被 terminated 或者 stopped
WCONTINUED: 等待子进程直接某个被 terminated 或者某个 stopped 子进程被
SIGCONT 唤醒。
wait(statusp): 等同于 waitpid(-1, statusp, 0)
kill(pid, sig):发送信号到进程 pid,失败返回 -1
kill(-pid, sig):发送信号到进程组 pid,失败返回 -1
alarm(secs):在 secs 秒之后向自己发送 SIGALARM 的定时信号
signal(sig, handler):将 handler 安装绑定到信号 sig 上,handler 可以使用 SIG_IGN
或 SIG_DFL 来指定忽略或者默认
sigprocmask(how, set, oldset):使用 set 来设置 signal mask,旧的 set 会通过
oldset 返回。其中 how 有三种模式,SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
示例
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &prev);
/* critical region without interruption from SIGING */
sigprocmask(SIG_SETMASK, &prev, NULL);
sigsuspend(mask):临时使用 mask 代替当前 signal mask,并挂起进程直接接收到一个
未被 block 的信号。一般用于等待某个信号的到达。一般需要先屏蔽信号以进行必
要初始化,然后再通过 sigsuspend 取消屏蔽以进行信号处理
示例
volatile sig_atomic_t pid;
sigchld_handler:
int olderrno = errno
pid = waitpid(-1, NULL, 0)
errno = olderrno
main:
sigprocmask(SIG_BLOCK, &mask, &prev); /* block SIGCHLD */
if (fork() == 0) /* child */
exit(0);
pid = 0;
while (!pid) /* wait for SIGCHLD */
/* cuz SIGCHLD is blocked, it can only be received CORRECTLY here */
sigsuspend(&prev)
sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIGCHLD */
sigaction(sig, act, oldact):用于显式指定 signal 的行为,以提高代码可移植性
setjmp(env) 会将当前的 calling environment 存入 env,返回 0。而 longjmp(env, rc) 可以 跳转到最近调用的 setjmp(env) 处返回,返回值为 rc。可以如下组合来模拟 try catch
switch(setjmp(env)) {
case 1:
/* catch exception #1 */
break;
case 2:
/* catch exception #2 */
break;
case 0:
/* try_body */
...
/* raise exception #1 */
longjmp(env, 1);
/* raise exception #2 */
longjmp(env, 2);
}
sigsetjmp(env, savesigs) 和 siglongjmp(env, rc) 是对应的 signal handler 版本。其中的 savesigs 是个 flag,用于标示是否需要保存当前的 signal mask 到 env 中。这对组合可以用 来实现在接收某些信号时从 signal handler 中跳转到 main 开始的初始化部分,从而 restart 整个程序。
-EOF-