异步通知
尽管大多数时候阻塞和非阻塞型操作的组合及select方法可以有效地查询设备,但某些时候用这种技术处理效率就不高了。为了启用异步通知,用户程序必须执行两个步骤。首先,它们必须为文件指定一个“属主”进程,使用系统调用fcntl执行F_SETOWN时,属主进程的ID号就被保存到filp->f_owner中,这上步目的是为了让内核知道应该通知哪个进程。然后为了真正启用异步通知机制,用户程序还必须在设备中设置FASYNC标志,这通过fcntl的F_SETFL命令完成。
并不是所有设备都支持异步通用功能,我们也可以选择不提供异步通知功能。应用程序通常假设只有套接字和终端才有异步通知能力。
当进程收到信号时,它并不知道是哪个输入文件有了新的输入。如果有多个文件可以异步通知输入的进程,则应用程序仍然必须借助于poll或select来确定输入的来源。
从驱动程序的角度考虑
驱动程序是怎样实现异步信号的,下面从内核的角度来看详细的操作过程:
F_SETOWN被调用时对filp->f_owner赋值,其它什么也不做。 在执行F_SETEL启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标志发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其正确响应。文件打开时,FANSYNC标志被默认为是清除的。一会再看这个方法的标准实现 当数据到达时,所有所有注册为异步通知的进程都会被发送一个SIGIO信号。第一步实现很简单。其它步骤则要涉及维护一个动态数据结构,以跟踪不同的异步读取进程,这种进程可能会有好几个。不过这个动态数据结构并不依赖于特定的设备,内核已经提供了一套合适的通用实现方法,无需为每个驱动程序写同一代码。
Linux的这种通用方法基于一个数据结构和两个函数(它们要在前面提到的第二步和第三步中调用)。数据结构为struct fasync_struct。和处理等待队列的方法类似,我们需要把一个该类型的指针插入特定的数据结构中去。
两个函数原型如下:
int fasync_helper(int fd, struct file (filp, int mode, struct fasync_struct **fa);当一个打开的文件的FASYNC标志被修改时,调用它以便从相关的进程列表中增加或删除文件
void kill_fasync(struct fasync_struct **fa, int sig, int band);在数据到时使用它通知所有相关的进程
当文件关闭是时必须调用fasync方法,以便从活动的异步读取进程列表中删除该文件。
定位设备
llseek实现
llseek方法实现了lseek和llseek系统调用。内核默认通过修改filp->f_pos而执行定位,filp->f_pos是文件的当前读写位置。为了使系统调用能正确工作,read和write方法必须通过更新它们收到的偏移量参数来配合。
下面是scull的一个简单例子
loff_t scull_llseek(struct file *filp, loff_t off, int whence) { struct scull_dev *dev = filp->private_data; loff_t newpos; switch(whence) { case 0: /* SEEK_SET*/ newpos = off; break; case 1: /* SEEK_CUR*/ newpos = filp->f_pos + off; break; case 2: /* SEEK_END */ newpos = dev->size + off; break; default: /*不应该发生*/ return -EINVAL; } if(newpos < 0) return -EINVAL; filp->f_pos = newpos; return newpos; }
上面的实现对scull是有意义的,因为它处理一个明确定义的数据区。然而大多数设备只提供了数据流(就像串口和键盘),而不是数据区,定位这些设备是没有意义的。在这种情况下不能简单地不声明llseek操作,因为默认是允许定位的。相反,我们应该在我们的open方法中调用nonseekable_open,以便通知内核设备不支持llseek:
int nonseekable_open(struct inode *inode, struct file *filp);
上述调用会把给定的filp标记为不可定位;这样,内核就不会让这种文件上的lseek调用成功。通过这种方式标记文件,我们可以确保通过pread和pwrite系统调用也不能定位文件。为了完整起见我们还应该将file_operations结构中的llseek方法设置为特殊的辅助函数no_llseek,该函数定义在<linux/fs.h>中。
设备文件的访问控制
提供访问控制对于设备节点的可靠性有时是到头重要的。如,不公不允许未授权的用户使用设备(这可以通过设置文件系统的许可位实现),而且在某些情况下一次只允许一个授权用户打开设备。
到现在为止,我们还没有看到能超越文件系统权限位而实现任意访问控制的代码。如果open系统调用将请求转给驱动程序,open就成功了。现在来介绍一些实现某些附加检查的技术。
独享设备(只由一个进程访问)scullsingle
最生硬的访问控制方法是一次只允许一个进程打开设备(独享)。最好避免使用这种技术,因为它制约了用户的灵活性。用户可能会希望在同一设备上运行不同的进程,一个用来读取状态信息,而另一个进程写入数据。
一次只允许一个进程打开设备有很多令人不快的特性,不过这也是设备驱动程序中最容易实现的访问控制。
下面代码摘自scullsingle设备:
static atomic_t scull_s_available = ATOMIC_INIT(1);//变量初始化为1
static int scull_s_open(struct inode *inode, struct file *filp) { struct scull_dev *dev = &scull_s_device; if(!atomic_dec_and_test (&scull_s_available)) { atomic_inc(&scull_s_available); return -EBUSY; /*已打开*/ } if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) { scull_trim(dev); } return 0; }
另一方面,release调用则标记设备为不再忙
static int scull_s_release(struct inode *inode, struct file *filp) { atomic_inc(&scull_s_available); /*释放设备*/ return 0; }
限制每次只由一个用户访问(可以多进程)sculluid
与独享相比,此时需要两个数据项:一个打开计数一个设备属主UID。
open调用第一次打开时授权,但它记录下设备的属主。这意味着一个用户可以多次打开设备,允许几个互相协作的进程并发地在设备上操作。同时其它用户不能打开这个设备,这就避免了外部干扰。
spin_lock(&scull_u_lock); if(scull_u_count && (scull_u_owner != current->uid) && /*允许用户*/ (scull_u_owner != current->euid) && /*允许su命令用户*/ !capable(CAP_DAC_OVERRIDE)) { /*也允许root用户*/ spin_unlock(&scull_u_lock); return -EBUSY; /*返回-EPERM会让用户混淆*/ } if(scull_u_count == 0) scull_u_owner = current->uid; scull_u_count++; spin_unlock(&scull_u_lock);
变量scull_u_owner和scull_u_count控制对设备的访问,并且可由多个进程并发地访问。为了让这些变量安全,我们通过一个自旋锁(scull_u_lock)来保护对这些变量的访问。这里采用自旋锁的原因在于,锁的拥有时间非常短,而在拥有锁的时间内,驱动程序不会做任何可能休眠的工作。
release方法如下:static int scull_u_release(struct inode *inode, struct file *filp) { spin_lock(&scull_u_lock); scull_u_count--;//除此之外不做任何事情 spin_unlock(&scull_u_lock); return 0; }
替代EBUSY的阻塞型open,scullwuid
当设备不能用时返回一个错误,通常这是最合理的方式,但有些情况下可能需要让进程等待设备。
例如,如果一个以周期性的、预定的方式发送定时报告的数据通道,在被人们根据需要临时使用时,那么在通道正忙的时候,定时报告最好能够稍微延迟一会儿,而不是因为通道忙就返回失败。
这是在设计设备驱动程序时程序员必须作出的选择,所解决的问题不同,答案也就不一样。
代替EBUSY的另一个方法是实现阻塞型open。scullwuid设备和sculluid不同的是,open时会等待设备而不是返回-EBUSY。它和sculluid只在open操作的下列部分不同:
spin_lock(&scull_w_lock); while(!scull_w_available()) { spin_unlock(&scull_w_lock); if(filp->f_flags & O_NONBLOCK) return -EAGAIN; if(wait_event_interruptible(scull_w_wait, scull_w_available())) return -ERESTARTSYS; /*告诉fs层做进一步处理*/ spin_lock(&scull_w_lock); } if(scull_w_count == 0) scull_w_count = current->uid; scull_w_count++; spin_unlock(&scull_w_lock);
这里的实现又是基于等待队列,下面是release方法唤醒所有等待进程:
static int scull_u_release(struct inode *inode, struct file *filp) { int temp; spin_lock(&scull_w_lock); scull_w_count--; temp = scull_w_count; spin_unlock(&scull_u_lock); if(temp == 0) wake_up_interruptible_sync(&scull_w_wait);/*唤醒所有其它的uid进程*/ return 0; }
阻塞开open的实现对于交互式用户来说它是令人不愉快的,用户可能会在等待中猜测设备出了什么问题。
这类问题(对同一设备的不同的,不兼容的策略)最好通过为每一种访问策略实现一个设备节点的方法来解决。
在打开时复制设备
另一个实现访问控制的方法是,在进程打开设备时创建设备的不同私有副本。
显然这种方法只有在设备没有绑定到某个硬件对象时才能实现。scull就是这样一个软件设备的例子
scullpriv设备open方法如下,它必须找到正确的终端,也许还需要创建一个。
/*和复制相关的数据结构包括一个key成员*/ struct scull_listitem{ struct scull_dev device; dev_t key; struct list_head list; }; /*设备的链表,以及保护它的锁*/ static LIST_HEAD(scull_c_list); static spinlock_t scull_c_lock = SPIN_LOCK_UNLOCKED; /*查找设备,如果没有就创建一个*/ static struct scull_dev *scull_c_lookfor_device(dev_t key) { struct scull_listitem *lptr; list_for_each_entry(lptr, &scull_c_list, list){ if(lptr->key == key) return &(lptr->device); } /*没有找到*/ lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL); if(!lptr) return NULL; /*初始化该设备*/ memset(lptr, 0 sizeof(struct scull_listitem)); lptr->key = key; scull_trim(&(lptr->device)); /*初始化*/ init_MUTEX(&(lptr->device.sem)); /*将其放到链表中*/ list_add(&lptr->list, &scull_c_list); return &(lptr->device); } static int scull_c_open(struct inode *inode, struct file *filp) { struct scull_dev *dev; dev_t key; if(!current->signal->tty) { PDEBUG("Process \"%s\" has no ctl tty\n", current->comm); return -EINVAL; } key = tty_devnum(current->signal->tty); /*在链表中查找scullc设备*/ spin_lock(&scull_c_lock); dev = scull_c_lockfor_device(key); spin_unlock(&scull_c_lock); if(!dev) return -ENOMEM; /*然后从裸的scull设备中复制所有其它数据*/ }
release方法中什么也没有做,以便在关闭设备后再打开时还能获得之前写入的数据
static int scull_s_release(struct inode *inode, struct file *filp) { return 0; }