驱动程序当设备有 I/O 事件发生,应有机制保证向登记的应用进程发送信号,显然设备驱动程序扮演了重要角色,实际终端 tty、网络 socket 等的驱动层标准实现已经包括了对实时信号驱动的支持,所以,在 Linux 应用程序中可以如上框架直接使用fcntl()配置。但有些设备的驱动程序还并没有支持此特征,对此,LDD [3] [4]都有描述(两个版本是一致的),能够提供一些重要的信息。以下两个 API 应该可以屏蔽所有相似操作(如send_sig()) 的标准接口:
1
2
3
| 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 上去。
1
2
3
4
5
6
7
8
9
10
| 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);
}
|
但做异步通知登记的进程又是怎么能知道事件的发生呢?应该是在确切知道事件发生的地方,那本源应该就是中断服务程序,或相应的软中断中发出信号通知。
1
2
| if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
|
如果是写操作,就是 POLL_OUT。不过,LDD 提醒大家在实现release方法(设备文件关闭)时,注意执行 fasync(),使得本文件的操作从上述的设备异步事件等待链表中剥离。
1
2
| /* 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第二版 就存在, 因为 2.4 的内核已经完全具有以上机制, 但在第三版 [4] 中仍未更正,希望注意。
相关内幕 为了进一步弄清信号通知机制,首先,让我们看一下 fs/fcntl.c, fcntl(fd, F_SETSIG, signum)到底做了什么:
1
2
3
4
5
6
7
8
| 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。
1
2
3
4
5
| /* 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),它的实现主如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| 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的要点,讨论了相关的系统内部,并纠正了一些现有的认识误区,希望彻底扫清理解和使用实时信号上的障碍。 |