36-可重入函数

这一篇,大家可以轻松下了。本篇的压力相对来说比较小。如果你编写过多线程程序,相信你听说过线程安全的函数,线程安全的函数是一种可重入函数。如果你从未接触过这个概念,也没什么关系。

本文我们不讲解线程安全函数,而是讲异步信号安全函数《可重入函数(二)》 中对比了线程安全函数与异步信号安全函数的异同点。

1. 何为可重入

不妨看下面的一个函数。

int a = 0; // 全局变量

int fun() {
  ++a;
  return a;
}

试想一下,当你在执行 fun() 函数的 return a 的时候(假设这时候 a 的值已经为 1),你的代码突然由于信号的打断而跳转到另一段代码运行。然而十分不巧的是,那段代码把 fun 函数执行了一遍(此时 a 的值已经变成了 2),当重新回到你的代码时,你的 fun 函数的返回值已经不再是你期望的 1,而是 2.

产生这种现象的本质在于,该函数引用了全局变量 a。

除此之外,使用静态局部变量也会出现这种问题。所以,我们把所有引用了全局变量静态变量的函数,称为不可重入函数,不可重入函数都不是信号安全的,也不是线程安全的。(有关线程,后面会慢慢涉及)。

反过来说,如果一个函数对于信号处理来说是可重入的,则称其为异步信号安全函数

注意:线程安全的函数,不一定是异步信号安全的。

有一点需要注意的是,如果一个函数使用了不可重入函数,那么该函数也会变成不可重入的。这意味着,你不能在信号处理函数中使用不可重入函数。

有很多 C 库函数和 linux 系统调用都是不可重入的,比如 malloc、getpwdnam。很多标准库的 IO 函数都是不可重入的,因为这些函数使用了缓冲区。

2. 不可重入导致的 bug

下面以实例说明在信号处理函数中使用不可重入函数带来的危害。

该段程序使用 getpwdnam 根据用户名获取用户 uid。在 main 函数中,getpwdnam 执行完后即进入 sleep 状态。另外该段程序注册了信号 SIGINT,待会我们在键盘键入Ctrl + C 命令以及什么都不做的情况下,看看屏幕打印的 uid 是多少。

  • 代码
// reenterable.c
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdio.h>

void handler(int sig) {
  getpwnam("root");
}

int main() {
  if (SIG_ERR == signal(SIGINT, handler)) {
    perror("signal");
    return 1;
  }
  printf("I'm %d\n", getpid());
  struct passwd *pwd = getpwnam("allen");
  sleep(10);
  printf("allen's uid = %d\n", pwd->pw_uid);
  return 0;
}
  • 编译
$ gcc reenterable.c -o reenterable
  • 运行
$ ./reenterable

2.1 结果分析

  • 当你运行程序后,什么也不做,等待 10 秒后,结果显示:
I'm 8055
allen's uid = 1000
  • 当程序在打印 uid 前,如果你按下 Ctrl + C,屏幕会打印:
I'm 8049
^Callen's uid = 0

这不符合我们的预期,uid = 0 明明是 root 账号的 uid 才对。主要原因是在 main 函数 sleep 的时候,我们向程序发送了 SIGINT 信号,程序即转入信号处理函数 handler 又执行了一次 getpwnam。

虽然很多时候这种 bug 很难出现,但是我们仍然要避免才对,我们不能把希望寄托在运气上。所以在编写信号处理函数的时候,一定要保证你的函数是可重入的。

3. 总结

  • 理解可重入、不可重入的含义
  • 知道信号处理函数要求是可重入的
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页