其它高级字符驱动程序操作

异步通知

尽管大多数时候阻塞和非阻塞型操作的组合及select方法可以有效地查询设备,但某些时候用这种技术处理效率就不高了。为了启用异步通知,用户程序必须执行两个步骤。首先,它们必须为文件指定一个“属主”进程,使用系统调用fcntl执行F_SETOWN时,属主进程的ID号就被保存到filp->f_owner中,这上步目的是为了让内核知道应该通知哪个进程。然后为了真正启用异步通知机制,用户程序还必须在设备中设置FASYNC标志,这通过fcntl的F_SETFL命令完成。

并不是所有设备都支持异步通用功能,我们也可以选择不提供异步通知功能。应用程序通常假设只有套接字和终端才有异步通知能力。

当进程收到信号时,它并不知道是哪个输入文件有了新的输入。如果有多个文件可以异步通知输入的进程,则应用程序仍然必须借助于poll或select来确定输入的来源。

从驱动程序的角度考虑

驱动程序是怎样实现异步信号的,下面从内核的角度来看详细的操作过程:

  1. F_SETOWN被调用时对filp->f_owner赋值,其它什么也不做。
  2. 在执行F_SETEL启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标志发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其正确响应。文件打开时,FANSYNC标志被默认为是清除的。一会再看这个方法的标准实现
  3. 当数据到达时,所有所有注册为异步通知的进程都会被发送一个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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值