RT-Signal 及其应用要点

http://www.ibm.com/developerworks/cn/linux/l-rtsignal/

本文指出 POSIX RT-Signal 在应用中的使用方法和在设备驱动程序编写中的要点,以及内核中对应的技术内幕,特别的,纠正了著作 Linux Device Driver [3][4]中相关的错误内容。实时信号驱动可以形成一个高性能的可伸缩 I/O 处理框架。

背景

著作 Unix Network Programming [9] I/O Models一节非常清晰地指出了应用中各种可能的 I/O 模型:

  • blocking I/O
  • non-blocking I/O
  • I/O multiplexing(select 和 poll)
  • signal driven I/O(SIGIO)
  • asynchronous I/O(POSIX aio_函数)

其中 signal driven 中的 signal 是指 Unix 信号,它有两个限制:

  • 大部分信号有专门用途,用户可定制使用的个数极少,主要是 SIGIO;
  • 信号没有附加信息,如果一个信号源有多种产生信号的原因,信号接收者无法确定究竟发生了什么。例如:对于 socket,I/O 完成意味着多种可能,对于 UDP socket 有两种可能,而 TCP socket 则有七种之多,这样应用程序收到 SIGIO,根本无从区分处理,甚至收到数据还是数据发出都不知道;

所以只能在特定情况下个别地应用这种机制。Unix Network Programming第三版 [10]于 2003 年底出版,但 signal 驱动的机制并未被更多地讨论。实际上Linux 从 2.3 的内核起,已经引入了 POSIX RT-Signal(Real-Time Signal) 机制。 解决了传统信号的局限,不但数量足够多,而且每个信号还可以携带相应的必要信息(Payload),这样,基于 RT-signal 的 I/O 框架可以处理来自不同设备的不同事件。关于使用 RT-Signal 的详细描述似乎并不多见,其中developerWorks 中国网站有作者曾撰文上下两篇 [7][8]介绍信号, 非常深入地介绍了应用程序中的信号编程,主要是从进程间通信角度出发,但并未涉及设备和 I/O。

I/O 与实时信号

根据 I/O 模型,当一个设备的 I/O 完成,应用进程可以通过 RT-Signal 获得异步通知,进行处理,Windows 中称为 I/O Completion Port [2]。如果程序处理的信号根源来自于设备,驱动程序一定扮演了重要的角色,而内核的在应用和驱动之间的联系机制也十分关键,那究竟是什么呢?在经典著作 Linux Device Driver [3]中有一节"异步通知"描述了相关内容,但未提及 RT-Signal。而 2005 年 2 月出的 LDD 第三版 [4],该节内容未做改动,第二版的相关错误也就未能改正:

There is one remaining problem with input notification. When a process receives a SIGIO, it doesn't know which input file has new input to offer. If more than one file is enabled to asynchronously notify the process of pending input, the application must still resort to poll or select to find out what happened.

这种理解是错误的,在应用中相应的使用指导也就不是完整的,这恐怕与作者未引入 RT-Signal 有关,也导致应用中的使用很有局限性,下面将会详细讨论原因。本文的讨论也助于理解 AIO 机制,signal 也是其基础。

实时信号与应用框架

在应用程序中使用实时信号(Real-Time Signal),应该如何进行呢?假设,相关的设备支持这个机制,那么它们的初始设置应有以下几步:

1. 与使用其它设备一样,应首先打开设备,比如键盘是 /dev/tty,视频采集卡是 /dev/video0, 而对网络略有不同,打开一个 socket 作为设备文件句柄;

2. 设置异步通知的相关属性;

3. 很重要的是,为了实现多设备异步事件到达时的差异性,应为不同设备的事件设置对应的不同实时信号值,这个数字从 SIGRTMIN(32) 到SIGRTMAX(63),传统的信号 0-31 可以被认为非实时信号;

4. 注意,因为使用 F_SETSIG,在该程序文件一开始,必须做如下声明。

/* We need F_SETSIG */
  #define _GNU_SOURCE 1

对于不同设备,这个过程都为如下处理:

            /* Open Deveice. socket() replaces open() if networking is involved */
      if(-1 == (fd = open(devicename, O_RDONLY))){
        perror("device open");
        return -1;
      }
      /* Tell the OS that we should get a signal for a particular process' 
         device handler */
      if (-1 == fcntl(fd, F_SETOWN, getpid())) {
        perror("fcntl F_SETOWN");
        return -1;
      }
      /* Set the FD nonblocking */
      flags = fcntl(fd, F_GETFL, 0);
      if (-1 == fcntl(fd, F_SETFL, flags | O_NONBLOCK | FASYNC)) {
        perror("fcntl F_SETFL");
        return -1;
      }
      /* FD should raise signum from SIGRTMIN when an event happens */
      if (-1 == fcntl(fd, F_SETSIG, signum)){
        perror("fcntl F_SETSIG");
        return -1;
      }

一旦这些打开的设备启动后,如果有数据到达,比如视频采集卡获得一帧图像, 本应用程序进程将捕捉到相应的信号,之前如果使用 signal() 和 sigaction()注册了回调函数,则会被调用,介绍这方面使用的资料较多,请查阅。这里将利用信号支持队列的属性,程序框架使用 sigwaitinfo() 系统调用:

            /* The idea here is to eliminate the cost and complexity of
      a jump into a signal handler. Instead we just dequeue signals
      as they become available */
      while (1) {
        /* This is a blocking call, we could use sigtimedinfo
        which takes a timeout value and then do other work
        in this loop, but we only have one FD. */
        if (sigwaitinfo(&blocked_sigs, &info) < 0) {
          if (errno != EINTR) {
            perror("sigwaitinfo");
            return 1;
          }
        }
        switch(handle_siginfo(&info))
        {
          case -1: 
          return 1;
          case 0: 
          return 0;
          default:
          break;
        }
      }

其中,变量blocked_sigs 就是定义了一个信号集合,下面的代码就是保证这些信号不使用回调机制,而是呆在队列里,由应用程序调用 sigwaitinfo() 提取。

            sigset_t blocked_sigs;
      /* SIGIO shouldn't be queued since it represents a queue overflow */
      sigemptyset(&sa.sa_mask);
      sa.sa_flags = 0;
      sa.sa_handler = sigio_handler;
      if (sigaction(SIGIO, &sa, 0) == -1) {
        perror("sigaction SIGIO");
        return 1;
      }        
      /* Queue these signals */
      sigemptyset(&blocked_sigs);
      sigaddset(&blocked_sigs, INTSIG); //就是传统信号SIGINT
      sigaddset(&blocked_sigs, TIMSIG); //定时器信号,SIGRTMIN + 1
      sigaddset(&blocked_sigs, KEYSIG); //键盘,      SIGRTMIN + 2
      sigaddset(&blocked_sigs, VIDSIG); //图像,      SIGRTMIN + 3
      sigaddset(&blocked_sigs, UDPSIG); //socket,    SIGRTMIN + 4
      sigprocmask(SIG_BLOCK, &blocked_sigs, &sa.sa_mask);

这里,将键盘、视频采集和 UDP 通讯对应的信号都放入该集合,还加入了中断信号(CTRL-C 产生的非实时信号)和定时器信号,所有该集合的信号统一使用上面的框架处理,进程的信号队列是一个优先级队列,信号值越小,将会插在队列的前面。如果队列溢出,会产生一个 SIGIO 信号,所以不要将它纳入到框架中,保证可以进行合适的异常处理。从进程信号队列中提取的信息,结构如下:

            struct siginfo { 
        int si_signo; //信号值
        int si_errno; 
        int si_code; 
        union { 
          /* other members elided */ 
          struct { 
            int _band; //
            int _fd;   //文件句柄
          } _sigpoll; 
        } _sifields; 
      } siginfo_t;
      struct pollfd { 
        int fd; 
        short events; 
        short revents; 
      };

它不仅含有发生的信号值,还有其它信息,所以这些值可以用来区分不同的设备发生的不同的事件,据此做出相应的处理,显然这彻底解决了传统信号的根本问题。对于一帧新到达的图像,应用进程得到通知后就可以去访问相应的缓冲区,进行视频处理。对于图像这样的大量数据,应用进程常常通过 mmap() 映射共享核态驱动程序的缓冲区,实际这正是 AIO 的效果。

驱动程序

当设备有 I/O 事件发生,应有机制保证向登记的应用进程发送信号,显然设备驱动程序扮演了重要角色,实际终端 tty、网络 socket 等的驱动层标准实现已经包括了对实时信号驱动的支持,所以,在 Linux 应用程序中可以如上框架直接使用fcntl()配置。但有些设备的驱动程序还并没有支持此特征,对此,LDD [3] [4]都有描述(两个版本是一致的),能够提供一些重要的信息。以下两个 API 应该可以屏蔽所有相似操作(如send_sig()) 的标准接口:

            int fasync_helper(int fd, struct file *filp, int mode, 
                        struct fasync_struct **fa);
      void kill_fasync(struct fasync_struct **fa, int sig, int band);

为了支持异步通知机制,设备结构中需要有异步事件通知队列,它应该与睡眠队列类似,并且需要实现 fasync 方法(method)。当一个打开的文件 FASYNC 标志变化时,如fcntl(fd, F_SETFL, flags | FASYNC)操作,它将被调用将相应的进程登记到 async_queue 上去。

            struct my_dev{
        wait_queue_head_t in, out;
        ...
        struct fasync_struct *async_queue;
      };
      static int my_f_fasync(int fd, struct file *filp, int mode)
      {
        struct my_dev *dev = filp->private_data;
        return fasync_helper(fd, filp, mode, &dev->async_queue);
      }

但做异步通知登记的进程又是怎么能知道事件的发生呢?应该是在确切知道事件发生的地方,那本源应该就是中断服务程序,或相应的软中断中发出信号通知。

            if (dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

如果是写操作,就是 POLL_OUT。不过,LDD 提醒大家在实现release方法(设备文件关闭)时,注意执行 fasync(),使得本文件的操作从上述的设备异步事件等待链表中剥离。

            /* remove this filp from the asynchronously notified filp's */
      my_f_fasync(-1, filp, 0);

至此,一个支持 RT-Signal 的驱动框架也就具备了。不过,LDD 的读者请注意以下几点:

  • kill_async() 并没有发出用户指定的信号值,而是发出 SIGIO, 这是一个兼容传统的做法,没有问题,但并不是说,用户进程将得到 SIGIO,如果用户设置了希望的信号值,用户进程将得到它,如 SIGRTMIN + 2 而不是 SIGIO,下一节将解释为什么;
  • 上一点是至关重要的,因为不能理解这一点,用户进程就没能使用 fcntl(fd, F_SETSIG, signum)指定信号值,那缺省值,的确就是 SIGIO 了;
  • 此外, LDD 中的例子是一个管道设备,管道读的信号通知时机由写决定,这不具有一般性,而中断服务程序或相关软中断中显然是最常见的适用时机;

以上问题,实际从 LDD第二版 [3] 就存在, 因为 2.4 的内核已经完全具有以上机制, 但在第三版 [4] 中仍未更正,希望注意。

相关内幕

为了进一步弄清信号通知机制,首先,让我们看一下 fs/fcntl.c, fcntl(fd, F_SETSIG, signum)到底做了什么:

            case F_SETSIG:
      /* arg == 0 restores default behaviour. */
      if (arg < 0 || arg > _NSIG) {
        break;
      }
      err = 0;
      filp->f_owner.signum = arg;
      break;

显然,在文件描述属性中有专门的成员变量存贮设定的信号值。当执行 kill_fasync() 时,将向该设备 async_queue 链表中的所有使用 fasync_help() 登记的进程发送信号,注意这里的从入口得到的 sig(一般为SIGIO)只作为不是 SIGURG 的指示,真正向进程发送的信号将为f_owner.signum。

              /* Don't send SIGURG to processes which have not set a
      queued signum: SIGURG has its own default signalling
      mechanism. */
      if (!(sig == SIGURG && fown->signum == 0))
        send_sigio(fown, fa->fa_fd, band);

send_sigio() 不仅只向进程发送一个信号, 还有相关的更多的信息(payload,siginfo_t),它的实现主如下:

        switch (fown->signum) {
      siginfo_t si;
      default:
        /* Queue a rt signal with the appropriate fd as its
        value.  We use SI_SIGIO as the source, not 
        SI_KERNEL, since kernel signals always get 
        delivered even if we can't queue.  Failure to
        queue in this case _should_ be reported; we fall
        back to SIGIO in that case. --sct */
        si.si_signo = fown->signum;
        si.si_errno = 0;
        si.si_code  = reason;
        /* Make sure we are called with one of the POLL_*
        reasons, otherwise we could leak kernel stack into
        userspace.  */
        if ((reason & __SI_MASK) != __SI_POLL)
          BUG();
        if (reason - POLL_IN >= NSIGPOLL)
          si.si_band  = ~0L;
        else
          si.si_band = band_table[reason - POLL_IN];
        si.si_fd    = fd;
        if (!send_sig_info(fown->signum, &si, p))
          break;
        /* fall-through: fall back on the old plain SIGIO signal */
      case 0:
        send_sig(SIGIO, p, 1);
    }

如果发送(向信号队列插入)失败,将发出一个传统的信号 SIGIO 给进程(上面的应用进程对此异常登记了回调函数予以处理:)。 至于 send_sig_info() 如何将信号加到进程的信号队列中,很容易理解,可以去阅读相关的代码,实际 [7] [8]有很好的描述,其中关于信号的可靠/不可靠讨论令人印象深刻。

结论

本文自顶而下又自底而上给出了用户程序和设备驱动支持 POSIX RT-Signal的要点,讨论了相关的系统内部,并纠正了一些现有的认识误区,希望彻底扫清理解和使用实时信号上的障碍。

侧记

  • Kernel 2.4 推出时,作者是一个图像处理程序员,开始做一个视频处理算法,本文的主要工作实际就是试验该算法的软件框架 [11]。当时直观地认为信号驱动是一个自底而上的、有效的软件框架,后来查到其它图像处理界的同行已做了类似的工作 [5],在一个圈子里传用,不过接口特定, 而 V4L(Video for Linux) 已成为视频采集方面的标准接口,所以就做了基于 V4L 相关的扩展;
  • 虽然驱动这个工作的是一个嵌入式项目,但一个关于服务器支持更多并发客户数的网站 [1]提供了相关最丰富的资源,也使我确认了RT-Signal驱动是最优的scalable IO框架之一 [6]。从嵌入式到服务器,难道还不够scalable?
  • 内核源代码的/usr/src/linux/Documentation目录下的dnotify.txt也是一个有趣的参考,它提供了文件/目录系统变更时用户程序得到异步通知的应用场景;
  • 纠正 Linux Device Driver 中的相关内容,实际不只是与时俱进的问题,主要是概念问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值