高级字符驱动一章介绍了编写全功能字符设备驱动程序的几个概念:1)用于设备控制的公共接口——ioctl系统调用;2)和用户空间保持同步的几种途径;3)在驱动程序中实现几种不同的设备访问策略。
一、ioctl
除了读写能力,大部分驱动程序还能执行各类型的硬件控制,比如,请求设备锁门、弹出介质、报告错误信息、改变波特率或执行自破坏等等。这些操作通常ioctl方法支持。
在用户空间,ioctl系统调用具有如下原型:
int ioctl(int fd,unsigned long cmd,...);
|
驱动程序的ioctl方法原型:
int (*ioctl)(struct inode *inode,struct file *filp, unsigned int cmd,unsigned long arg);
|
其中参数cmd由用户空间不经修改地传递给驱动程序,可选的arg参数则无论用户程序使用的是指针还是整数值,它都以unsigned long的形式传递给驱动程序。如果调用程序没有传递第三个参数,那么驱动程序所接收的arg参数处在未定义状态。如果ioctl传递一个非法参数,编译器是无法报警的,这样,相关联的程序错误就很难发现。
选择ioctl命令为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一。从include/asm/ioctl.h头文件中,我们可以得出,cmd为一个32位的无符号整数,被划分成4个段,具体表示如下:
cmd |
direction
(bit31--bit30)
|
size
(bit29--bit16)
|
type
(bit15--bit8)
|
number
(bit7--bit0)
|
所占位数 |
2 |
14 |
8 |
8 |
作用
|
命令:区别读/写 |
数据大小 |
幻数:表示设备类型 |
命令顺序序号 |
在头文件中构造如下的宏是要求掌握的:构造命令编号的宏: _IO(type,nr) /* 构造无参数的命令的命令编号 */ _IOR(type, nr, datatype) /* 构造从驱动程序中读取数据的命令编号 */ _IOW(type,nr,datatype) /* 写数据的命令 */
_IOWR(type,nr,datatype) /* 双向传输 */
/* type和number位字段通过参数传入, size字段通过对datatype参数取 sizeof 获得 */
解开位字段的宏:
_IOC_DIR(nr) _IOC_TYPE(nr) _IOC_NR(nr) _IOC_SIZE(nr)
|
返回值ioctl的实现通常就是一个命令号的switch语句,当命令不匹配任何合法命令的操作时,如该如何选择?有些内核会返回-ENVAL(非法参数),这是一种普遍的做法。而POSIX标准规定,应该返回-ENOTTY。
使用ioctl参数在使用ioctl的可选arg参数时,如果传递的是一个整数,它可以直接使用。如果是一个指针,就必须小心。当用一个指针引用用户空间, 我们必须确保用户地址是合法的,可通过函数access_ok验证地址(而不是传递参数),该函数中声明:int access_ok(int type, const void *addr, unsigned long size); |
第一个参数应当是 VERIFY_READ或VERIFY_WRITE,取决于要执行的动作是读取还是写入用户空间内存区;addr 参数为用户空间地址,size
为字节数。如果在指定地址处既要读取又要写入,则应该用VERIFY_WRITE。access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0
是失败(存取有问题)。如果它返回假,驱动应当返回-EFAULT给调用者。
对于access_ok,有两点值得注意:
1)access_ok并没有完成验证内存的全部工作,而只检查所引用内存是否位于进程有对应访问权限的区域内,特别是要确保访问地址没有指向内核空间。
2)大部分驱动代码不需要真正调用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它们带有校验的功能,确保进程能够写入给定的内存地址,成功时返回 0, 并且在错误时返回 -EFAULT。
put_user(datum, ptr)
__put_user(datum, ptr)
get_user(local, ptr)
__get_user(local, ptr)
|
这些宏它们相对copy_to_user 和copy_from_user快,
并且这些宏已被编写来允许传递任何类型的指针,只要它是一个用户空间地址. 传送的数据大小依赖 prt 参数的类型, 并且在编译时使用
sizeof 和 typeof 等编译器内建宏确定。他们只传送1、2、4或8 个字节。如果使用以上函数来传送一个大小不适合的值,结果常常是一个来自编译器的奇怪消息,如"coversion to non-scalar type requested". 在这些情况中,必须使用 copy_to_user 或者 copy_from_user。
__put_user和__get_user进行更少的检查(不调用
access_ok),如果地址指向用户不能写入的内存,也会出现操作失败。所以他们都应该在已经使用access_ok检查过内存区后再使用。作为通用的规则:当实现一个 read 方法时,可以调用 __put_user 来节省几个时钟周期,
或者在复制多项数据之前调用一次access_ok。权能与受限操作对设备的访问由设备文件的权限控制,驱动程序通常不进行权限检查,不过有些情况,程序驱动必须进行附加的检查以确认用户是否有权进行请求的操作。linux内核提供了一个更为灵活的系统,称为权能。它能使某个特定的用户或程序可以被授权执行某一指定的特权操作,同时又没有执行其他不相关操作的能力。内核专为许可管理使用权能并导出两个系统调用capget和capset,这样就可以从用户空间来管理权能。
全部权能操作都可以在
中找到,其中包含了系统能够理解的所有权能:
CAP_DAC_OVERRIDE /* 越过文件和目录的访问限制(数据访问控制或 DAC)的能力。*/
CAP_NET_ADMIN /* 执行网络管理任务的能力, 包括那些能够影响网络接口的任务 */
CAP_SYS_MODULE /* 加载或去除内核模块的能力 */
CAP_SYS_RAWIO /* 进行" 裸 "I/O操作的能力。如访问设备端口或直接与USB设备通讯 */
CAP_SYS_ADMIN /* 截获的能力, 提供对许多系统管理操作的途径 */
CAP_SYS_TTY_CONFIG /* 执行 tty 配置任务的能力 */ |
在执行一项特权操作之前,设备驱动程序应该检查调用进程是否有合适的权能。
权能检查通过capable函数实现(定义在
中):
int capable(int capability);
|
非ioctl的设备控制
有时通过向设备写入控制序列可以更好地控制设备。在控制台驱动程序中就使用了这一技术,称为“转义序列”,用于控制移动光标、改变默认颜色或其他的配备任务。这种方法实现设备控制的好处是,用户仅通过写数据就可以控制设备,无需使用(有时还得编写)配置设备的程序。
通过写入来控制的方式非常适合于那种不传送数据而只响应命令的设备。
二、阻塞型I/O
当驱动程序无法立即满足调用进程的请求时,默认情况下,驱动程序应该阻塞该进程,将其置入休眠状态直到请求可继续。本小节将关注如何使进程进入休眠,又如何在将来将其唤醒。
休眠简介
当一个进程被置入休眠时,它会被标记为一种特殊状态并从调度器的运行队列中移走。休眠中的进程会被搁置在一边,等待某个事情发生修改了那个状态,进程才会在任意CPU上调度,也即运行该进程。
为了将进程以一种安全的方式进入休眠,我们需要牢记两条规则:
1) 永远不要在原子上下文中进入休眠。即驱动程序不能在拥有自旋锁、seqlock或RCU锁时休眠;如果禁止了中断也不能休眠。在拥有信号量休眠是合法,但是必须仔细检查拥有信号量的线程时休眠的代码。如果代码在拥有信号时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有信号量而休眠的代码必须很短,并且还要确保拥有信号量并不会阻塞最终会唤醒我们自己的那个进程。
2)当进程被唤醒时,无法知道休眠了多长时间、休眠期间发生了什么事情;也无法知道是否还有其他进程在同一事件上休眠可能会提前唤醒而将等待的资源拿走。因此必须检查以确保进程的条件真正为真。
除非确信其他进程会在其他地方唤醒休眠的进程,否则也不能睡眠。使进程可被找到意味着:需要维护一个称为等待队列的数据结构。它是一个进程链表,其中饱含了等待某个特定事件的所有进程。在 Linux 中,一个等待队列由一个wait_queue_head_t结构体来管理,其定义在中。初始化一个等待一个等待队列头:
// 静态方式: DECLARE_WAIT_QUEUE_HEAD(name);
// 动态方式: wait_queue_head_t my_queue; init_waitqueue_head(&my_queue);
|
简单休眠
当一个休眠进程被唤醒时,它必须再次检查它所等待的条件的确为真。linux内核中最简单的休眠方式是称为wait_event的宏以及它的几个变种:
wait_event(queue, condition) /* 不可中断休眠,不推荐 */ wait_event_interruptible(queue, condition) /* 最好的选择方式,返回非零值意味着休眠被某个信号中断,且驱动也许要返回 -ERESTARTSYS */ wait_event_timeout(queue, condition, timeout) wait_event_interruptible_timeout(queue, condition, timeout) /* 有限的时间的休眠;若超时,则不管条件为何值返回0 */ |
休眠的进程需要其他线程(另一进程或中断)来唤醒,唤醒休眠进程的函数称是wake_up,形式如下:
void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue)
|
在实践中,约定作法:在使用wait_event时使用wake_up;使用wait_event_interruptible时使用wake_up_interruptible。
阻塞和非阻塞型操作
在默认情况下执行阻塞型操作,应该实现下列动作以保持和标准语义一致:
- 如果一个进程调用了read但是还没有数据可读,此进程必须阻塞。数据到达进程被唤醒,并把数据返回给调用者。
- 如果一个进程调用了write但缓冲区没有空间,此进程必须阻塞,而且必须休眠在与读取进程不同的等待队列上。录和硬件设备写入一些数据,从而腾出了部分输出缓冲区后,进程即被唤醒,write调用成功。
有时调用进程会不想阻塞,而不管其I/O是否可以继续。显示的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定。这个标志在中定义,这个头文件自动包含在中。
如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单返回-EAGAIN。
非阻塞型操作会立即返回,使得应用程序可以查询数据。在处理非阻塞型文件时,应用程序调用stdio函数必须非常小心,因为很容易把一个非阻塞返回误认为EOF,所以必须始终检查errno。
O_NONBLOCK用在open调用可能会阻塞很长时间的场合也是很有意思的。
高级休眠
进程如何休眠
前面介绍提到了wait_queue_head_t结构体,它由一个自旋锁和一个链表组成,具体形式如下:
struct wait_queue_head_t { spinlock lock; struct list_head task_list; }
|
其中链表中保存的是一个等待队列入口,该入口声明为wait_queue_t类型。
将进程置于休眠的步骤:
1) 分配并初始化一个wait_queue_t结构,然后加入到对应的等待队列。
2) 设置进程的状态,将其标记为休眠。
在 中定义了多个任务状态:TASK_RUNNING表示进程可运行。有两个状态表明进程处于休眠状态: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE,它们分别对应于两种休眠。2.6 内核的驱动代码通常不需要直接操作进程状态。但如果需要这样做使用的代码是:
void set_current_state(int new_state); |
在老的代码中, 常常见到这样的语句:current->state =
TASK_INTERRUPTIBLE; 不鼓励这种方式直接修改current,很容易导致代码无法运行,且修改进程当前状态的代码并不会将自己置于休眠状态。
3) 最后一步是放弃处理器。 但必须先检查进入休眠的条件。如果不做检查会引入竞态: 如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下:
if (!condition) schedule();
|
如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为TASK_RUNNING,还必要从等待队列中去除这个进程,否则它可能被多次唤醒。
手工休眠
1)建立并初始一个等待队列入口:
DEFINE_WAIT(my_wait); 或者 wait_queue_t my_wait; init_wait(my_wait);
|
2) 将等待队列入口添加到队列中去,并设置进程的状态。通过一个函数就可以完成:
void prepare_to_wait(wait_queue_head_t *queue, // 等待队列头 wait_queue_t *wait, // 进程入口 int state); // 进程新状态
|
在调用此函数之后,进程即可调用schedule,当然在此之前,应确保仍有必要等待。
3) 一旦schedule返回,就到了清理时间,调用函数:
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
|
独占等待
在许多情况下:当某个进程在等待队列上调用wait_up时,所有等待在该队列上的进程都将置为可运行状态。
但有这么种情况:预先知道只会有一个进程被唤醒的进程可以获得期望的资源,而其他被唤醒的进程只会再次休眠。如果等待队列中的队列中的进程数量非常庞大,将严重影响系统性能。
为解决此问题,内核开发者为内核增加了“独占等待”选项。一个独占等待的行为和通常的休眠类似,但有两个重要不同:
1)等待队列入口设置了WQ_FLAG_EXCLUSIEV标志时,则会被添加到等待队列的尾部。而没有这个标志的入口会被添加到头部。
2) 在某个等待队列上调用wait_up时,它会在唤醒第一个具有WQ_FLAG_EXCLUSIEV标志的进程之后停止唤醒其他进程。
最终结果是,执行独占等待的进程每次只会被唤醒其中一个,但内核每次会唤醒所有非独占等待进程。
驱动程序采用独占等待需要满足两个条件:
1)对某个资源存在严重竞争。
2)唤醒单个进程就能完整消耗该资源。
将进程置于可中断独占等待调用函数:
void prepare_to_wait_exclusive(wait_queue_head_t *queue wait_queue_t *wait int state);
|
该函数可用来替换prepare_to_wait,它设置等待队列入口的“独占”标志,并将进程添加到等待队列的尾部。注意wait_event及其变种无法执行独占等待。
唤醒的相关细节
当一个进程被唤醒时,实际的结果由等待队列入口中的一个函数控制。默认的唤醒函数将进程置为可运行状态,并且如果该进程具有更高的优先级,则会执行一次上下文切换以便切换到该进程。不同的唤醒函数,可参阅
wake_up(wait_queue_head_t *queue); wake_up_interruptible(wait_queue_head_t *queue); /* wake_up
唤醒队列中的所有非独占等待进程和一个独占等待进程。wake_up_interruptible 同样,
除了它跳过处于不可中断休眠的进程。它们在返回之前, 使一个或多个进程被唤醒、被调度(如果它们被从一个原子上下文调用, 这就不会发生)。 */
wake_up_nr(wait_queue_head_t *queue, int nr); wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); /* 这些函数类似 wake_up, 除了它们能够唤醒nr个独占等待进程, 而不只是一个. 注意传递0表明请求唤醒所有的独占等待进程,而不是不唤醒任何一个。 */
wake_up_all(wait_queue_head_t *queue); wake_up_interruptible_all(wait_queue_head_t *queue); /* 这种 形式 wake_up唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程) */
wake_up_interruptible_sync(wait_queue_head_t *queue); /*
一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果调用wake_up的进程运行在原子上下文中,不希望被调度出处理器时,可以使用
wake_up_interruptible 的" sync "变体。 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。 */
|
三、poll和select
poll方法是poll、epoll和select这三个系统调用的后端实现,它们可用来查询某个或多个文件扫描符上的读取或写入是否被阻塞。poll方法应该返回一个位掩码,用来指示出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。如果驱动程序将poll方法定义为NULL,则设备会被认为既可读也可写,并且不会被阻塞。
三个系统调用均通过驱动程序的poll方法提供。该方法具有如下的原型:
unsigned int(*poll)(struct file *filp, poll_table *wait);
|
其中的参数poll_table结构(在中声明)被传递给驱动程序方法,以使每个可以唤醒进程和修改poll操作状态的等待队列都可以被驱动程序装载。
当用户空间程序在驱动程序关联的文件描述符执行poll、epoll和select系统调用时,该驱动程序方法将被调用。该设备方法分为两步处理:
1) 在一个或多个可指示poll状态变化的等待队列上调用poll_wait。如果当前没有文件描述符可用来执行I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。
通过poll_wait函数,驱动程序向poll_table结构添加一个等待队列:
void poll_wait(struct file *,wait_queue_head_t *,poll_table *);
|
2) 返回一个用来描述操作是否可以立即无阻塞执行的位掩码。
位掩码 |
含义 |
POLLIN |
设备可无阻塞读取 |
POLLRDNORM
|
若“通常”的数据已就绪,可以读取,就设置该位。一个可读设备返回POLLIN|POLLRDNORM |
POLLRDBAND
|
可以从设备读取频带之外的数据。 |
POLLPRI
|
可以无阻塞地读取高优先级(即频带之外)的数据 |
POLLHUP |
当读取设备的进程到达文件尾时,设置该位。 |
POLLERR
|
设备发生了错误 |
POLLOUT
|
设备可以无阻塞地写入 |
POLLWRNORM
|
若“通常”的数据已就绪,可以写入,就设置该位。一个可读设备返回POLLOUT|POLLWRNORM |
POLLWRBAND
|
具有非零优先级的数据可以被写入设备。 |
与read和write的交互
poll和select调用的目的是确定接下来的I/O操作是否会阻塞。更重要的用途是它们可以使应用程序同时等待多个数据流。
正确实现上面三个调用是非常重要的。总结一下使用规则:
从设备读取数据:
1) 如果输入缓冲区有数据,那么即使就绪的数据比程序所请求的少,并且驱动程序保证剩下的数据马上就能到达,read调用仍然应该难以察觉的延迟立即返回。如果为了某种方便,read甚至可以一直返回比所请求数目少的数据,前提是至少得返回一个数据。
2) 如果输入缓冲区中没有数据,那么默认情况下read必须阻塞等待,直到至少有一个字节到达。另一方面,如果设置了O_NONBLOCK标志,read应立即返回,返回是-EAGAIN。在这种情况下poll必须报告设备不可读,直到至少有一个字节到达。
3) 如果已经到达文件尾,read应该立即返回0,无论O_NONBLOCK是否设置。此时poll应该报告POLLHUP。
向设备定入数据:
1)如果输出缓冲区中有空间,则write应该无延迟地立即返回。它可以接收比请求少的数据,但至少要接收一个字节。此情况下,poll报告设备可写。
2) 如果输出缓冲区已满,那么在默认情况下write被阻塞直到有空间释放。如果设置了O_NONBLOCK标志,write应立即返回,返回-EAGAIN。这时poll应报告文件不可写。另一方面,如果设备不能再接受任何数据,则write返回—ENOSPC,不管O_NONBLOCK是否设置。
3) 永远不要让write调用在返回等待数据的传输结束,即使O_NONBLOCK标志被清除。这是因为许多应用程序用selet来检查write是否会阻塞。如果报告设备可写,调用就不能阻塞;如果使用设备的程序需要保证输出缓冲区中的数据确实已经被传送出去,驱动程序就必须提供一个fsync方法。
刷新待处理输出
write方法不能满足所有数据输出的需求,fsync函数可以弥补这一空隙,它通过同名系统调用来调用,该方法原型:
int (*fsync)(struct file *file,struct dentry *dentry,int datasync);
|
如果应用程序需要确保数据已经被传送到设备上,就必须实现fsync方法。一个fysnc调用只有在设备已被完全刷新(输出缓冲区全空)时才会返回,即使要花一些时间。是否设置了O_NONBLOCK标志对此没影响。
底层的数据结构
当用户应用程序调用了poll、select或epoll_ctl函数时,内核会调用由该系统调用引用的全部文件的poll方法,并向它们传递一个poll_table。poll_table结构是构成实际数据结构的一个简单封装:
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
typedef struct poll_table_struct { poll_queue_proc qproc; } poll_table;
|
由上面的代码可以知道poll_table结构只是对一个函数的封装,更有趣的是,这个函数建立了实际的数据结构。那个数据结构, 对于 poll和 select,是一个内存页的链表, 其中包含 poll_table_entry 结构。每个 poll_table_entry 持有被传递给
poll_wait 的 struct file 和 wait_queue_head_t 指针, 以及一个关联的等待队列入口.
下面是poll_table_entry结构体的源代码:
struct poll_table_entry { struct file * filp; wait_queue_t wait; wait_queue_head_t * wait_address; }; |
对poll_wait的调用有时还会将进程添加到给定的等待队列。整个的结构必须由内核维护,在poll或者 select返回前,进程可从所有的队列中去除。
如果被轮询的驱动没有一个驱动程序指明可进行非阻塞I/O,poll调用会简单地睡眠,直到一个它所在的等待队列(可能许多)唤醒它。当poll调用完成,poll_table结构被重新分配, 所有的之前加入到poll表的等待队列入口都会从表及它们的等待队列中移出。
四、异步通知
通过使用异步通知,应用程序可以在数据可用时收到一个信号,而不需要不停地使用轮询来关注数据。
为了启用文件的异步通知机制,用户程序必须执行两个步骤:
1)它们指定一个进程作为文件的“属主(owner)”。当进程使用fcntl系统调用执行F_SETOWN命令时,属主进程的进程ID就被保存在filp->f_owner中。目的是为了让内核知道应该通知哪个进程。
2)为了真正启用异步通知机制,用户程序还必须在设备中设置FASYNC标志,这通过fcntl的F_SETFL命令完成。
执行这两个步骤之后,输入文件就可以在新数据到达时请求发送一个SIGIO信号。该信号被发送到存放在filp->fowner中的进程。
从驱动程序的角度考虑
对我们来讲,一个更重要的话题是驱动程序怎样实现异步信号。下面列出的是从内核角度来看的详细操作过程:
1) F_SETOWN被调用时对filp->f_owner赋值,此外什么了不做。
2) 在执行F_SETFL启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标志发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其能正确响应。文件打开时,FASYNC标志被默认是清除的。
3) 当数据到达时,所有注册为异步通知的进程都会被发送一个SIGIO信号。
Linux的这种通用方法基于一个数据结构和两个函数,相关声明的头文件是。
数据结构为struct fasync_struct:
struct fasync_struct { int magic; int fa_fd; struct fasync_struct *fa_next; /* singly linked list */ struct file *fa_file; }; |
两个函数:
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标志被修改是地,调用fasync_helper以便从相关的进程列表中增加或删除文件。除了最后一个参数外,它的其他所有参数都是提供给fasync方法的相同参数,因此可直接传递。在数据到达时,可使用kill_fasync
通知所有的相关进程同,其参数包括要发送的信号(通常为SIGIO)和带宽(几乎为POLL_IN)。
当文件关闭时必须调用fasync方法,以便从活动的异步读取进程列表中删除该文件。尽管这个调用只在filp->f_flags设置了FASYNC标志时才是必需的,但不管什么情况,调用它不会有什么问题,且这也是最普通的实现方法。
异步通知所使用的数据结构和struct wait_queue使用的几乎是相同的,都涉及等待事件。不同之处在于前者struct file替换了struct task_struct。队列中的file结构用来获取f_owner,以便给进程发送信号。
五、定位设备
llseek实现
llseek方法实现了lseek和llseek系统调用。如果设备操作未定义llseek方法,内核默认通过修改filp->f_pos而执行定位,filp->fpos是文件的当前读取/写入位置。为了使lseek系统调用能正确工作,read和write方法必须通过更新它们收到的偏移量参数来配合。
如果定位操作对应于设备的一个物理操作,可能就需要提供自己的llseek方法。
内核默认是允许定位的,而对于只提供数据流的设备,定位是没有意义的。这种情况下不能简单地不声明llseek操作,而是在open方法中调用nonseekable_open,以便通知内核设备不支持llseek:
int nonseekable_open(struct inode *inode; struct file *filp); |
上述调用将会反给定的filp标记为不可定位,这样内核就不会让这种文件的上lseek调用成功。
为了完整起见,还应该将file_operations结构中的llseek方法设备为特殊的辅助函数no_llseek,该函数定义在中。
六、设备文件的访问控制
提供访问控制对于设备节点的可靠性有时是至关重要。比如,不仅不允许未授权的用户使用设备,而且在某些情况下一次只能允许一个授权用户打开设备。
独享设备
最生硬的访问控制方法是一次只允许一个进程打开设备。最好避免使用这种技术,因为它制约了用户的灵活性。独享设备方式其实建立了一种策略,而这种策略妨碍了用户完成他的工作。一次只允许一个进程打开设备有很多令人不快的特性,不过这也是设备驱动程序中最容易实现的访问控制方法。
单用户访问
在构造独享设备之后,我们要建立允许单个用户在多个进程中打开的设备,但每次只允许一个用户打开该设备。这种方案便于测试该设备,因为用户每次可从多个进程读取和写入,前提是由用户负责在多进程访问中维护数据的完整性。这通过在open方法中加入检查来完成。
与独享策略相比,实现这些访问策略需要两个数据项:一个打开计数和设备属主的UID。
open调用在第一次打开时授权,但它记录下设备的属主。这意味着一个用户可以多次打开设备,允许几个互相协作的进程并发地在设备上操作。同时,其他用户不能打开这个设备,避免了外部干扰。
阻塞open
当设备不能访问时返回一个错误,通常这是最合理的方式,但有些情况下可能需要让进程等待设备。该方式可用阻塞型open实现,open时会等待设备而不是返回-EBUSY。
阻塞弄open实现中的问题是,对一交互式用户来说它是令人很不愉快的。用户可能会在等待中猜测设备出了什么问题。这类问题最好通过为每一种访问策略实现一个设备节点的方法来解决。
打开是复制设备
实现访问控制的另一方法:在进程打开设备时创建设备的不同私有副本。
这种方法只有在设备没有绑定到某个硬件对象时才能实现。/dev/tty 的内部使用类似的技术来给它的进程一个不同的 /dev 入口点所呈现的“景象”。这类访问控制较少见,但这个实现可说明内核代码可以轻松改变应用程序的运行环境。
阅读(2708) | 评论(0) | 转发(0) |