Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9164586
  • 博文数量: 1727
  • 博客积分: 12961
  • 博客等级: 上将
  • 技术积分: 19860
  • 用 户 组: 普通用户
  • 注册时间: 2009-01-09 11:25
个人简介

偷得浮生半桶水(半日闲), 好记性不如抄下来(烂笔头). 信息爆炸的时代, 学习是一项持续的工作.

文章分类

全部博文(1727)

文章存档

2024年(3)

2023年(26)

2022年(112)

2021年(217)

2020年(157)

2019年(192)

2018年(81)

2017年(78)

2016年(70)

2015年(52)

2014年(40)

2013年(51)

2012年(85)

2011年(45)

2010年(231)

2009年(287)

分类: LINUX

2009-02-06 12:08:53

 

 

 

Abstract:

本文指出RT Signal在应用中的使用方法和在设备驱动程序编写中的要点, 以及内核中对应的技术内幕, 特别的,

纠正了Linux Device Driver中相关的错误内容。 实时信号驱动可以形成一个高性能的可伸缩I/O处理框架。

KeyWords: Linux, signal, real-time, scalable I/O

 背景- Background

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

  • blocking I/O
  • nonblocking I/O
  • I/O multiplexing(selectpoll)
  • signal driven I/O(SIGIO)
  • asynchronous I/O(POSIX aio_函数)

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

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

所以只能在特定情况下个别地应用这种机制。Unix Network Programming第三版 [10]2003年底出版,

signal驱动的机制并未被更多地讨论。 实际上Linux2.3的内核起,已经引入了POSIX RT-Signal(Real-Time Signal)机制。

解决了传统信号的局限,不但数量足够多,而且每个信号还可以携带相应的必要信息(Payload), 这样,基于RT-signalI/O

框架可以处理来自不同I/O设备、不同的事件。 关于使用RT-Signal的详细描述似乎并不多见, 其中developerWorks中国网

站有作者曾撰文上下两篇 [7] [8]介绍信号, 非常深入地介绍了应用程序中的信号编程, 主要是从进程间通信角度出发,但并

未涉及设备和I/O

 I/O与实时信号- RT-Signal

根据I/O模型,当一个设备的I/O完成,应用进程通过RT-Signal获得异步通知,进行处理, Windows中称为I/O Completion Port [2]

 如果程序处理的信号根源来自于设备,驱动程序一定扮演了重要的角色, 而内核的在应用和驱动之间的联系机制也十分关键,那究竟是

什么呢? 在经典著作Linux Device Driver2nd [3]中有一节“Asynchronous Notification(异步通知)”描述了相关内容, 但未提

RT-Signal。而20052月出的LDD第三版 [4], 并未对第二版(针对kernel 2.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.

这种理解是错误的,也导致应用中的使用很有局限,如认为信号值只能固定在SIGIO, 这恐怕与该书未引入RT-Signal有关,

下面将会详细讨论原因。 本文的讨论也助于理解AIO机制,signal是最有效的通知基础。

 实时信号与应用框架- RT-Signal Framework

在应用程序中使用实时信号(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_的效果。

驱动程序- Device Driver

当设备有IO事件发生,就有机制保证向应用进程发送信号, 显然设备驱动程序扮演重要角色,

实际终端tty、网络socket等的标准实现已经包括了实时信号驱动的支持, 所以,在Linux中它

们可以如上直接使用。 但有些设备的驱动程序还并没有支持,所以需要定制设备驱动程序, 对此

,文献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), 它将被调用,将本进程登记到async_queue上去。

struct my_dev{        
wait_queue_head_t in, out;        ...        
struct fasync_struct *async_queue;      
};      
 
/* fasync method */      
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);      
}

但信号又是在事件发生时产生呢呢?应该是在确切知道事件发生的时机, 就是中断服务程序或相

应的软中断中调用kill_fasync()

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

如果是写操作,就是POLL_OUT。 注意,无论用户进程设定了什么期望的信号, 在这个环节,发送的一般就是SIGIO

 

LDD提醒读者在设备文件关闭(release方法)时,注意执行fasync, 使得本文件的操作从上述的设

备异步事件等待链表中剥离。

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

不过,LDD读者也请注意以下几点:

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

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

 相关内幕- Internals

为了进一步弄清,首先,让我们看一下系统源代码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;

在文件属性中有专门元素signum存贮设定的信号值。 当执行kill_fasync()时,将向async_queue
链表中的所有使用 fasync_help()登记的进程发送信号,如下代码, 注意这里从函数入口得到的
sig
(一般为SIGIO)实际只作为不是SIGURG的指示, 真正向进程发送的信号由决定。

/* 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)

 
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]有很好的描述, 特别是,其中关于信号的可靠/不可靠问题的讨论令人印象深刻。

 结论- Conclusion

本文给出了从设备驱动到应用程序使用POSIX实时信号需要注意的要点, 纠正了一些常见的错误提法,

讨论了相关的一些系统内部, 希望彻底扫清RT-Signal应用的障碍。

 侧记- Side Story

  • Kernel 2.4推出时,作者是一名做图像处理的程序员,开始做一个视频处理算法,
    试验该算法的软件框架和BT8x8 frame grabber的驱动程序定制就成为本文的主要内容 [11]
     
    当时直观地认为信号驱动是一个直接、有效的软件框架, 也查到其它图像处理界的前辈做过类似的工作 [5]
    他们没有基于V4L(Video For Linux),只是在一个小圈子里传用,接口特定,
    V4L已成为视频采集方面的标准接口,所以作者就做了基于V4L相关的扩展,
    不过,现在看比较粗糟;
  • 虽然驱动这个工作的是一个嵌入式项目, 一个关于服务器支持更多并发数的网站 [1]
    却提供了相关最丰富的资源, 从中也使我确认了RT-Signal驱动是最好的可伸缩
    IO
    框架之一 [6]。 从嵌入式到服务器,I/O性能都是最优,难道还不足够scalable
  • 纠正经典之作Linux Device Driver的相关内容,实际不是与时俱进的问题,主要是概念问题。

(出处:网侠)

 

 

 

 

 

阅读(2362) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~