博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
《Linux Device Driver》——高级字符驱动程序操作
阅读量:4157 次
发布时间:2019-05-26

本文共 29832 字,大约阅读时间需要 99 分钟。

首先实现ioctl系统调用,它是用来设备控制的公共接口。

然后,内核态与用户态保持同步。
掌握进程休眠和唤醒、实现阻塞I/O,以及在设备可读取或写入时通知用户空间。
最后,在驱动程序中实现设备访问策略。


ioctl

驱动程序通过ioctl执行各种类型的硬件控制。

1.在用户空间,ioctl系统调用具有如下原型:

int ioctl(int fd,unsigned long cmd,...);
参数 描述
fd 文件描述符
cmd 控制命令
表示可变数目的参数表,在实际使用中是一个可选参数,习惯上用char* argp定义。

每个ioctl命令就是一个独立的系统调用,而且是非公开的。

2.驱动程序中

int (*ioctl) (struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg);
参数 描述
inode 对应于应用程序传递的文件描述符fd,这与传给open方法的参数一致
filp 对应于应用程序传递的文件描述符fd,这与传给open方法的参数一致
cmd 由用户空间直接不经修改的传递给驱动程序
arg 可选 以unsigned long的形式传递给驱动程序

选择ioctl命令

#include/asm/ioctl.h和Documentation/ioctl-number.txt

ioctl命令号定义:type、number、direction、size

参数 描述 位数
type 幻数。选择一个号码,并在整个驱动程序中使用这个号码 8bit(_IOC_TYPEBITS)
number 序数。顺序编号 8bits(_IOC_NRBITS)
direction 涉及数据传输时该字段定义数据传输的方向,涉及内容包括_IOC_NONE(无数据传输),_IOC_READ(从设备中读),_IOC_WRITE,_IOC_READ|_IOC_WRITE(双向数据传输)。数据传输是从应用程序的角度来看的,_IOC_READ意味着从设备中读取数据,所以应用程序必须向用户空间写入数据
size 表示所涉及的用户数据大小 通常为13位或是14位,具体可通过宏_IOC_SIZEBITS找到针对特定体系结构的具体数值。内核不会检查这个位字段,对该字段的检查可以帮助我们检测用户空间的错误。

<asm/ioctl.h>定义了一些构造命令编号的宏:

_IOR(type,nr,datetype) 构造从驱动程序中读取数据的命令
_IO(type,nr) 用于构造无参数的命令编号
_IOW(type,nr,datetype) 用于写入数据的命令编号
_IOWR(type,nr,datatype) 双向传输

type,number通过参数传入,size通过对datatype参数取sizeof获取

还有一些解开位字段的宏:_IOC_DIR(nr),_IOC_TYPE(nr),_IOC_NR(nr),_IOC_SIZE(nr)

_IOC_NR : 读取基数域值 (bit0~ bit7)
_IOC_TYPE : 读取魔数域值 (bit8 ~ bit15)
_IOC_SIZE : 读取数据大小域值 (bit16 ~ bit29)
_IOC_DIR : 获取读写属性域值 (bit30 ~ bit31)

scull的scull.h中有一些关于ioctl命令定义,这些命令是用来设置或获取驱动程序的配置参数:

#define SCULL_IOC_MAGIC  'k'/* Please use a different 8-bit number in your code */#define SCULL_IOCRESET    _IO(SCULL_IOC_MAGIC, 0)/* * S means "Set" through a ptr, * T means "Tell" directly with the argument value * G means "Get": reply by setting through a pointer * Q means "Query": response is on the return value * X means "eXchange": switch G and S atomically * H means "sHift": switch T and Q atomically */#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC,  1, int)#define SCULL_IOCSQSET    _IOW(SCULL_IOC_MAGIC,  2, int)#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC,   3)#define SCULL_IOCTQSET    _IO(SCULL_IOC_MAGIC,   4)#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC,  5, int)#define SCULL_IOCGQSET    _IOR(SCULL_IOC_MAGIC,  6, int)#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC,   7)#define SCULL_IOCQQSET    _IO(SCULL_IOC_MAGIC,   8)#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)#define SCULL_IOCXQSET    _IOWR(SCULL_IOC_MAGIC,10, int)#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC,  11)#define SCULL_IOCHQSET    _IO(SCULL_IOC_MAGIC,  12)

通过指针和显式的数值来实现整数参数的传递。

通过指针和显式的数值来实现返回值。

返回值 描述
返回值为正 正常工作
返回值为负 错误,设置用户空间的errno变量

返回值

对非法的ioctl命令一般会返回-EINVAL


预定义命令

在使用ioctl命令编号时,一定要避免与预定义命令重复,否则,命令冲突,设备不会响应。

下列ioctl命令对任何文件(包括设备特定文件)都是预定义的:

预定义命令 描述
FIOCTLX 设置执行时关闭标志
FIONCLEX 清除执行时关闭标志
FIOASYNC 设置或复位文件异步通知
FIOQSIZE 返回文件或目录大小,用于设备文件时会导致ENOTTY错误的返回
FIONBIO 文件非阻塞型IO,file ioctl non-blocking i/o

使用ioctl参数

参数类型 描述
arg整数 直接用
arg指针 需检测后才能用

使用指针,首先得保证指针指向的地址(用户空间)合法。因此,在使用这个指针之前,我们应该使用access_ok函数来验证地址的合法性:

#include
int access_ok(int type,const void *addr,unsigend long size) 返回值为1(成功)或0(失败),如果返回失败,驱动程序通常返回-EFAULT给调用者。

type: VERIFY_READ 或是 VERIFY_WRITE,取决于是读取还是写入用户空间内存区。

addr: 一个用户空间的地址。
size: 如果要读取或写入一个int型数据,则为sizeof(int)。
如果在该地址处既要读取,又要写入,则应该用:VERIFY_WRITE,因为它是VERIFY_READ的超集。

注意:首先, access_ok不做校验内存存取的完整工作; 它只检查内存引用是否在这个进程有合理权限的内存范围中,且确保这个地址不指向内核空间内存。其次,大部分驱动代码不需要真正调用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它们带有校验的功能,确保进程能够写入给定的内存地址,成功时返回 0, 并且在错误时返回 -EFAULT。

scull中main.c通过分析ioctl编号的位字段来检查参数:

int scull_ioctl(struct inode *inode, struct file *filp,                 unsigned int cmd, unsigned long arg){
int err = 0, tmp; int retval = 0; /* * extract the type and number bitfields, and don't decode * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok() */ if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY; if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY; /* * the direction is a bitmask, and VERIFY_WRITE catches R/W * transfers. `Type' is user-oriented, while * access_ok is kernel-oriented, so the concept of "read" and * "write" is reversed */ if (_IOC_DIR(cmd) & _IOC_READ) err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd)); else if (_IOC_DIR(cmd) & _IOC_WRITE) err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd)); if (err) return -EFAULT; ...}
#include
put_user(datum,ptr);/*进行检查以确保进程可以写入指定的内存地址,成功返回0,出错返回-EFAULT*/__put_user(datum,ptr);/*使用时,速度快,不做类型检查,使用时可以给ptr传递任意类型的指针参数,只要是个用户空间的地址就行,传递的数据大小依赖于ptr参数的类型*/get_user(datum.ptr);__get_user(datum,ptr);/*接收的数据被保存在局部变量local中,返回值说明其是否正确。同样,__get_user应该在操作地址被access_ok后使用*/

put_user vs __put_user:使用前做的检查,put_user多些,__put_user少些,

这些宏把datum写入到用户空间。当传递单个数据时,应用这些宏而不是copy_to_user。
一般用法:实现一个读取方法时,可以调用__put_user来节省几个时钟周期,或者在复制多项数据之前调用一次access_ok,像上面代码一样。


权能与受限操作

#include<linux/capability.h>

来由:驱动程序必须进行附加的检查以确认用户是否有权进行请求的操作
权能作用:基于权能的系统抛弃了那种要么全有,要么全无的特权分配方式,而是把特权操作划分成了独立的组。

#include
int capable(int capability);

在执行一项特权之前,应先检查其是否具有这个权利:

if (! capable (CAP_SYS_ADMIN)) return -EPERM;
权能 描述
CAP_DAC_OVERRIDE 越过在文件和目录上的访问限制(数据访问控制或 DAC)的能力
CAP_NET_ADMIN 进行网络管理任务的能力, 包括那些能够影响网络接口的任务
CAP_SYS_MODULE 加载或去除内核模块的能力
CAP_SYS_RAWIO 进行 “raw”(裸)I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯
CAP_SYS_ADMIN 截获的能力, 提供对许多系统管理操作的途径
CAP_SYS_TTY_CONFIG 执行 tty 配置任务的能力

ioctl命令的实现

scull的ioctl实现只传递设备的可配置参数:

switch(cmd) {
case SCULL_IOCRESET: scull_quantum = SCULL_QUANTUM; scull_qset = SCULL_QSET; break; case SCULL_IOCSQUANTUM: /* Set: arg points to the value */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; retval = __get_user(scull_quantum, (int __user *)arg); break; case SCULL_IOCTQUANTUM: /* Tell: arg is the value */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; scull_quantum = arg; break; case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */ retval = __put_user(scull_quantum, (int __user *)arg); break; case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */ return scull_quantum; case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */ if (! capable (CAP_SYS_ADMIN)) case SCULL_IOCTQUANTUM: /* Tell: arg is the value */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; scull_quantum = arg; break; case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */ retval = __put_user(scull_quantum, (int __user *)arg); break; case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */ return scull_quantum; case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; retval = __get_user(scull_quantum, (int __user *)arg); if (retval == 0) retval = __put_user(tmp, (int __user *)arg); break; case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; scull_quantum = arg; return tmp; case SCULL_IOCSQSET: if (! capable (CAP_SYS_ADMIN)) case SCULL_IOCGQSET: retval = __put_user(scull_qset, (int __user *)arg); break; case SCULL_IOCQQSET: return scull_qset; case SCULL_IOCXQSET: if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_qset; retval = __get_user(scull_qset, (int __user *)arg); if (retval == 0) retval = put_user(tmp, (int __user *)arg); break; case SCULL_IOCHQSET: if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_qset; scull_qset = arg; return tmp; /* * The following two change the buffer size for scullpipe. * The scullpipe device uses this same ioctl method, just to * write less code. Actually, it's the same driver, isn't it? */ case SCULL_P_IOCTSIZE: scull_p_buffer = arg; break; case SCULL_P_IOCQSIZE: return scull_p_buffer; default: /* redundant, as cmd was checked against MAXNR */ return -ENOTTY; } return retval;

非ioctl的设备控制

使用“转义字符”控制移动光标、改变默认颜色或执行其他配置任务。好处是用户仅通过写数据就能控制设备,无需使用配置设备的程序。


阻塞型I/O

当数据不可用时,用户可能调用read;或者进程试图写入数据时,但因为输出缓冲区已满,设备还未准备好接受数据。驱动程序应该(默认)阻塞该进程,将其置于休眠状态直到请求可以继续。


休眠

当一个进程被置入休眠时,会被标记为一种特殊状态并从调度器的运行队列中移走。直到某些情况下修改了这个状态,进程才会在任意CPU上调度,即运行该进程。休眠中的进程会被搁置在一边,等待将来某个事件发生。

安全进入休眠两原则:

1.永远不要在原子上下文中进入休眠。(原子上下文:在执行多个步骤时,不能有任何的并发访问。这意味着,驱动程序不能在拥有自旋锁,seqlock,或是RCU锁时,休眠;并且如果已经禁止了中断,也不能休眠)
2.对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真。

要休眠进程,必须有一个前提:有人能够唤醒进程,而且这个人必须知道在哪能唤醒进程。这里引入“等待队列”的概念。

等待队列:就是一个休眠进程链表,其中包含等待某个特定事件的所有进程。
等待队列头:一个等待队列通过一个“等待队列头(wait queue head)”来管理,等待队列头是一个wait_queue_head_t的结构体,定义在<linux/wait.h>。
静态定义并初始化一个等待队列头:

DECLARE_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,conditon,timeout);/*有限的时间的休眠,若超时,则不管条件为何值返回0*/

在上面所有的形式中,queue是等待队列头。是通过值传递而不是指针,condition是任意一个布尔表达式,上面的宏都在休眠前后对该表达式求值;在条件为真之前,进程保持休眠。后面两个变形会等待给定时间,当时间到期时,无论condition如何求值,都会返回0。

其他执行线程必须唤醒休眠进程。

唤醒休眠进程的函数是:wake_up:

void wake_up(wait_queue_head_t  *queue);/*唤醒等待在给定queue上的所有进程*/void wake_up_interruptible(wait_queue_head  *queue);/*只唤醒执行中断休眠的进程*/

惯例:用wake_up唤醒wait_event,用wake_up_interruptible唤醒wait_event_interruptible

休眠与唤醒实例:

/*任何从该设备上读取的进程均被置于休眠。只要某个进程向给设备写入,所有休眠的进程就会被唤醒*/static DECLARE_WAIT_QUEUE_HEAD(wq);static int flag =0;ssize_t sleepy_read(struct file *filp,char __user *buf,size_t count,loff_t *pos){
pirntk(KERN_DEBUG "process %i (%s) going to sleep\n",current->pid,current->comm); wait_event_interruptible(wq,flag!=0); flag=0; printk(KERN_DEBUG "awoken %i (%s) \n",current->pid,current->comm); return 0;}ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos){
printk(KERN_DEBUG "process %i (%s) awakening the readers ...\n",current->pid,current->comm); flag=1; wake_up_interruptible(&wq); return count; /*成功并避免重试*/}

阻塞和非阻塞型操作

操作系统中睡眠、阻塞、挂起的区别形象解释首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能怪雇工,肯定你这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。

全功能的read和write方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义在<linux/fcntl.h> ,被<linux/fs.h>自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容System V代码。O_NONBLOCK标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠。

其实不一定只有read和write方法有阻塞操作,open也可以有阻塞操作。

1.如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单的返回-EAGAIN。
2.非阻塞型操作会立即返回,使得应用程序可以查询数据。在处理非阻塞型文件时,应用程序调用stdio函数必须非常小心,因为很容易就把一个非阻塞返回误认为EOF,所以必须始终检查errno。
3.有些驱动程序还为O_NONBLOCK实现了特殊的语义。例如,在磁带还没有插入时打开一个磁带设备通常会阻塞,如果磁带驱动程序使用O_NONBLOCK打开的,则不管磁带在不在,open都会立即成功返回。
4.只有read,write,open文件操作受非阻塞标志的影响。


一个阻塞I/O示例

在驱动程序内部,阻塞在read调用的进程在数据到达时被唤醒;通常硬件会发出一个中断来通知事件,然后作为中断处理的一部分,驱动程序会唤醒等待进程。

本次例程中pipe.c选择使用另一个进程来产生数据并唤醒读取进程;类似的,读取进程用来唤醒等待缓冲区空间可用的写入进程。

该设备驱动程序使用了一个包含两个等待队列和一个缓冲区的设备结构。缓冲区大小可以用通常的方式配置(在编译、加载或运行时)。

struct scull_pipe {
wait_queue_head_t inq, outq; /* read and write queues */ char *buffer, *end; /* begin of buf, end of buf */ int buffersize; /* used in pointer arithmetic */ char *rp, *wp; /* where to read, where to write */ int nreaders, nwriters; /* number of openings for r/w */ struct fasync_struct *async_queue; /* asynchronous readers */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */};

read实现负责管理阻塞型和非阻塞型的输入:

static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,                loff_t *f_pos){
struct scull_pipe *dev = filp->private_data; if (down_interruptible(&dev->sem)) return -ERESTARTSYS; while (dev->rp == dev->wp) {
/* nothing to read */ up(&dev->sem); /* release the lock */ if (filp->f_flags & O_NONBLOCK) return -EAGAIN; PDEBUG("\"%s\" reading: going to sleep\n", current->comm); if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp))) return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ /* otherwise loop, but first reacquire the lock */ if (down_interruptible(&dev->sem)) return -ERESTARTSYS; } /* ok, data is there, return something */ if (dev->wp > dev->rp) count = min(count, (size_t)(dev->wp - dev->rp)); else /* the write pointer has wrapped, return data up to dev->end */ count = min(count, (size_t)(dev->end - dev->rp)); if (copy_to_user(buf, dev->rp, count)) {
up (&dev->sem); return -EFAULT; } dev->rp += count; if (dev->rp == dev->end) dev->rp = dev->buffer; /* wrapped */ up (&dev->sem); /* finally, awake any writers and return */ wake_up_interruptible(&dev->outq); PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count); return count;}

高级休眠


进程如何休眠

进程休眠步骤:

1.分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列。
2.设置进程的状态,将其标记为休眠。
在 <linux/sched.h> 中定义有几个任务状态:TASK_RUNNING意思是进程可运行。有 2 个状态指示一个进程是在睡眠: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.建立并初始化一个等待队列入口。

//方法1:DEFINE_WAIT(my_wait);//方法2: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);/*queue和wait分别是等待队列头和进程入口,state是进程的新状态,它应该是TASK_INTERRUPTIBLE(可中断休眠)或者TASK_UNINTERRUPTIBLE(不可中断休眠)*/

3.在调用prepaer_to_wait之后,进程即可调用schedule,当然在这之前,应确保仍有必有等待。一旦schedule返回,就到了清理时间了。

void finesh_wait(wait_queue_head_t *queue,wait_queue_t *wait);

scull中的write方法使用了prepare_to_wait和finish_wait。通常,不应该在同一个驱动中混合使用两种方法:

/* How much space is free? */static int spacefree(struct scull_pipe *dev){
if (dev->rp == dev->wp) return dev->buffersize - 1; return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;}static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
struct scull_pipe *dev = filp->private_data; int result; if (down_interruptible(&dev->sem)) return -ERESTARTSYS; /* Make sure there's space to write */ result = scull_getwritespace(dev, filp); if (result) return result; /* scull_getwritespace called up(&dev->sem) */ /* ok, space is there, accept something */ count = min(count, (size_t)spacefree(dev)); if (dev->wp >= dev->rp) count = min(count, (size_t)(dev->end - dev->wp)); /* to end-of-buf */ else /* the write pointer has wrapped, fill up to rp-1 */ count = min(count, (size_t)(dev->rp - dev->wp - 1)); PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf); if (copy_from_user(dev->wp, buf, count)) {
up (&dev->sem); return -EFAULT; } dev->wp += count; if (dev->wp == dev->end) dev->wp = dev->buffer; /* wrapped */ up(&dev->sem); /* finally, awake any reader */ wake_up_interruptible(&dev->inq); /* blocked in read() and select() */ /* and signal asynchronous readers, explained late in chapter 5 */ if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count); return count;}

将休眠的代码放在了独立的scull_getwritespace函数中。该函数确保新数据有可用的缓冲区空间,并在必要时休眠,知道新空间可用。一旦缓冲区空间可用,scull_p_write即可将用户数据复制到其中,调整指针,并唤醒可能正在等待数据的任何进程。

处理真正休眠的代码:

/* Wait for space for writing; caller must hold device semaphore.  On * error the semaphore will be released before returning. */static int scull_getwritespace(struct scull_pipe *dev, struct file *filp){
while (spacefree(dev) == 0) {
/* full */ DEFINE_WAIT(wait); up(&dev->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; PDEBUG("\"%s\" writing: going to sleep\n",current->comm); prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE); if (spacefree(dev) == 0) schedule(); finish_wait(&dev->outq, &wait); if (signal_pending(current)) return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ if (down_interruptible(&dev->sem)) return -ERESTARTSYS; } return 0;}

只要测试发生在进程已经将自己放在等待队列并修改了其状态之后,就不会出现任何问题。


等待独占

当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重降低系统性能。为此,内核开发者增加了一个“独占等待”选项。

一个独占等待的行为与通常的休眠类似,但有两个不同:

1.当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。
2.当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒。

执行独占等待的进程的最终结果是每次只会被唤醒其中一个(以某种有序方式),从而不会产生“疯狂兽群”问题。但是,内核每次仍然会唤醒所有非独占等待进程。

采用独占等待要满足 2 个条件:

1.希望对资源进行有效竞争;
2.当资源可用时,唤醒一个进程就足够来完全消耗资源。

使一个进程进入独占等待,可调用:

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

注意:无法使用 wait_event 和它的变体来进行独占等待.


唤醒的相关函数

很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:

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_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/

poll和select

当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读写阻塞的状态下,并影响了其它文件的读写。为了避免这种情况的发生,则必须使用多输入输出流又不想阻塞在他们任何一个上的应用程序常将非阻塞的 I/O和poll(system V),select(BSD Unix),epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标识读写相应的文件,实现非阻塞的读写。这些系统调用功能相同:允许进程来决定它是否可读或写一个或多个文件而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中的poll方法的支持。poll返回不同的标识,告诉主进程文件是否可以读写,其原型:

#include
unsigned int (*poll) (struct file *filp,poll_table *wait);

当用户空间程序在驱动程序关联的文件描述符上执行poll、select或epoll系统调用时,该驱动程序方法将被调用。该设备方法分两步:

1.在一个或多个可指示查询状态变化的等待队列上调用poll_wait,如果没有文件描述符可用来执行I/O,内核使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。
2.返回一个用来描述操作是否可以立即无阻塞执行的位掩码。

通过poll_wait函数,驱动程序向poll_table结构添加一个等待队列:

void poll_wait(struct file *,wait_queue_head_t *,poll_table *);

几个位掩码标志(通过<linux/poll.h>定义)用来指示可能的操作:

标志 含义
POLLIN 如果设备无阻塞的读,就设置该值
POLLRDNORM 通常的数据已经准备好,可以读了,就设置该值。通常的做法是会返回(POLLLIN
POLLRDBAND 如果可以从设备读出out-of-band(带外)数据,就设置该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中
POLLPRI 如果可以无阻塞的读取高优先级(out-of-band)数据,就设置该值,返回该值会导致select报告文件发生异常,以为select“out-of-band”外数据当作异常处理
POLLHUP 当读设备的进程到达文件尾时,驱动程序必须设置该值,依照select的功能描述,调用select的进程被告知进程时可读的。
POLLERR 如果设备发生错误,就设置该值。
POLLOUT 如果设备可以无阻塞地写,就设置该值
POLLWRNORM 设备已经准备好,可以写了,就设置该值。通常地做法是(POLLOUT|POLLNORM)
POLLWRBAND 于POLLRDBAND类似

POLLRDBAND和POLLWRBAND只在与套接字相关的文件描述符中才有意义。与设备驱动关系不大。

使用举例:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait){
struct scull_pipe *dev = filp->private_data; unsigned int mask = 0; /* * The buffer is circular; it is considered full * if "wp" is right behind "rp" and empty if the * two are equal. */ down(&dev->sem); poll_wait(filp, &dev->inq, wait); poll_wait(filp, &dev->outq, wait); if (dev->rp != dev->wp) mask |= POLLIN | POLLRDNORM; /* readable */ if (spacefree(dev)) mask |= POLLOUT | POLLWRNORM; /* writable */ up(&dev->sem); return mask;}

与read和write交互

正确实现poll调用的规则:

从设备读取数据:

1.如果在输入缓冲中有数据,read 调用应当立刻返回,即便数据少于应用程序要求的,并确保其他的数据会很快到达。 如果方便,可一直返回小于请求的数据,但至少返回一个字节。在这个情况下,poll 应当返回 POLLIN|POLLRDNORM。

2.如果在输入缓冲中无数据,read默认必须阻塞直到有一个字节。若O_NONBLOCK 被置位,read 立刻返回 -EAGIN 。在这个情况下,poll 必须报告这个设备是不可读(清零POLLIN|POLLRDNORM)的直到至少一个字节到达。

3.若处于文件尾,不管是否阻塞,read 应当立刻返回0,且poll 应该返回POLLHUP。


向设备写数据

1.若输出缓冲有空间,write 应立即返回。它可接受小于调用所请求的数据,但至少必须接受一个字节。在这个情况下,poll应返回 POLLOUT|POLLWRNORM。

2.若输出缓冲是满的,write默认阻塞直到一些空间被释放。若 O_NOBLOCK 被设置,write 立刻返回一个 -EAGAIN。在这些情况下, poll 应当报告文件是不可写的(清零POLLOUT|POLLWRNORM). 若设备不能接受任何多余数据, 不管是否设置了 O_NONBLOCK,write 应返回 -ENOSPC(“设备上没有空间”)。

3.永远不要让write调用在返回前等待数据传输的结束,即使O_NONBLOCK标志被清除。这是因为很多应用程序使用select来检查write是否会阻塞。如果报告设备可以写入, 调用就不能被阻塞。如果使用设备的程序需要保证输出缓冲区中的数据确实已经被传送出去,驱动程序就必须提供一个fsync方法。


刷新待处理输出

若一些应用程序需要确保数据被发送到设备,就必须实现fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被设置,即便这需要一些时间。其原型是:

int(*fsync)(structfile*file,struct dentry* dentry,int datasync);

底层数据结构

在这里插入图片描述


异步通知

通过使用异步通知,应用程序可以在数据可用时收到一个信号,而无需不停地轮询。

启用步骤:

1.它们指定一个进程作为文件的拥有者:使用 fcntl 系统调用发出 F_SETOWN 命令,这个拥有者进程的 ID 被保存在 filp->f_owner。
目的:让内核知道信号到达时该通知哪个进程。

2.使用 fcntl 系统调用,通过 F_SETFL 命令设置 FASYNC 标志。

执行完这两个步骤后,输入文件就可以在新数据到达时请求发送一个SIGIO信号。该信号被发送到存访filp->f_owner中的进程(如果是负值就是进程组)。

用户程序中的如下代码段启用了stdin输入文件到当前进程的异步通信机制:

signal(SIGIO,&input_handler);/*虚构示例,最好使用sigation()*/fcntl(STDIN_FILENO,F_SETOWN,getpid());oglags=fcntl(SDTIN_FILENO,F_GETEL);fcntl(STDIN_FILENO,F_SETFL,oflags|FASYNC);

从驱动程序的角度考虑

1.F_SETOWN被调用时filp->f_owner被赋值。

2. 当 F_SETFL 被执行来打开 FASYNC, 驱动的 fasync 方法被调用.这个标志在文件被打开时缺省地被清除。
3. 当数据到达时,所有的注册异步通知的进程都会被发送一个 SIGIO 信号。

Linux 提供的通用方法是基于一个数据结构和两个函数,定义在<linux/fs.h>。

这个数据结构称为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,structfile*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)和 band(几乎都是 POLL_IN)。

scull中pipe.c中这样实现fasync方法:

static int scull_p_fasync(int fd, struct file *filp, int mode){
struct scull_pipe *dev = filp->private_data; return fasync_helper(fd, filp, mode, &dev->async_queue);}

当数据到达, 下面的语句必须被执行来通知异步读者。因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生, 这个语句出现在 scullpipe 的 write 方法中:

if (dev->async_queue) kill_fasync(&dev->async_queue,SIGIO, POLL_IN); /* 注意,一些设备也针对设备可写而实现了异步通知,在这个情况,kill_fasnyc 必须以 POLL_OUT 模式调用.*/

当文件被关闭时必须 调用fasync 方法,来从活动的异步读取进程列表中删除该文件。尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。 以下是 scullpipe 的 release 方法的一部分:

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

异步通知使用的数据结构和 struct wait_queue几乎相同,因为他们都涉及等待事件。区别异步通知用 struct file 替代 struct task_struct。队列中的 file 用获取 f_owner,一边给进程发送信号。


定位设备

llseek实现

llseek方法实现了lseek和llseek系统调用。如果设备操作未定义llseek方法,内核默认通过修改file->f_pos而执行定位,file->f_pos是文件的当前读取/写入位置。为了使lseek系统调用能正确工作,read和write方法必须通过更新它们收到的偏移量参数来配合。

如果定位操作对应于设备的一个物理操作,就需要提供自己的llseek方法。

scull的main.c中这样定义:

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: /* can't happen */ return -EINVAL; } if (newpos < 0) return -EINVAL; filp->f_pos = newpos; return newpos;}/*这里唯一与设备相关的操作就是从设备中获得长度*/

大多数设备只提供了数据流(就像串口和键盘),而不是数据区,定位这些设备是没有意义的。

这种情况下的默认方法是允许定位。、
在open方法中调用nonseekable_open,以便通知内核设备不支持llseek:

int nonseelable_open(struct inode *inode;struct file *filp);/*会把给定的filp标记为不可定位;内核就不会让这种文件上的lseek调用成功。通过这种方式标记文件,可以确保pread和pwrite系统调用也不能定位文件*/

设备文件的访问控制

独享设备

一次只允许一个进程打开设备时是驱动程序最容易实现的访问控制方法。

scull设备维护一个atmoic_t变量,称为scull_s_available。该变量的值初始化为1,表明该设备真正可用。open调用会减少并测试scull_s_available,并在其他进程已经打开该设备时拒绝访问:

static struct scull_dev scull_s_device;static atomic_t scull_s_available = ATOMIC_INIT(1);static int scull_s_open(struct inode *inode, struct file *filp){
struct scull_dev *dev = &scull_s_device; /* device information */ if (! atomic_dec_and_test (&scull_s_available)) {
atomic_inc(&scull_s_available); return -EBUSY; /* already open */ } /* then, everything else is copied from the bare scull device */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) scull_trim(dev); filp->private_data = dev; return 0; /* success */}

realease调用则标记设备不再忙。

static int scull_s_release(struct inode *inode, struct file *filp){
atomic_inc(&scull_s_available); /* release the device */ return 0;}

限制每次只由一个用户访问

在构造独享设备之后,我们建立允许单个用户在多个进程中打开的设备,但是每次允许一个用户打开该设备。

与独享策略相比,此时需要两个数据项:一个打开计数和设备属主的UID。

open调用在第一次打开时授权,但记录下设备的属主。这意味着一个用户可以多次打开设备,允许几个互相协作的进程并发地在设备上操作。同时,其他用户不能打开这个设备,避免了外部干扰。

spin_lock(&scull_u_lock);if(scull_u_count&&		(scull_u_owner!=current->uid)&&	/*允许用户*/		(scull_u_owner!=current->current->euid)&& /*允许执行su命令的用户*/		!capable(CAP_DAC_OVERRIDE))	{
/*允许root用户*/ spin_unlock(&scull_u_lock);}if(scull_u_count==0) scull_u_owner=current->uid; /*获得所有者*/scull_u_count++;spin_unlock(&scull_u_lock);

sculluid有两个变量(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

如果一个以周期性、预定的方式发送定时报告的数据通道,同时也能根据人们的需要而临时使用,那么在通道正忙的时候,定时报告最好能稍微延迟一会儿,而不是因为通道忙就返回失败。


在打开时复制设备

另一种实现访问控制的方法是,在进程打开设备时创建设备的不同私有副本。

这种方法只有在设备没有绑定到某个硬件对象时才能实现。

/* The list of devices, and a lock to protect it */static LIST_HEAD(scull_c_list);static spinlock_t scull_c_lock = SPIN_LOCK_UNLOCKED;/* A placeholder scull_dev which really just holds the cdev stuff. */static struct scull_dev scull_c_device;/* Look for a device or create one if missing */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); } /* not found */ lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL); if (!lptr) return NULL; /* initialize the device */ memset(lptr, 0, sizeof(struct scull_listitem)); lptr->key = key; scull_trim(&(lptr->device)); /* initialize it */ init_MUTEX(&(lptr->device.sem)); /* place it in the list */ 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); /* look for a scullc device in the list */ spin_lock(&scull_c_lock); dev = scull_c_lookfor_device(key); spin_unlock(&scull_c_lock); if (!dev) return -ENOMEM; /* then, everything else is copied from the bare scull device */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) scull_trim(dev); filp->private_data = dev; return 0; /* success */}

release方法没做什么特殊的处理,通常在最后一次关闭时释放设备。

如果设备在最好一次关闭时被释放了,则在写入设备后将不能再从中读出同样的数据,除非有一个后台进程保持打开它。

static int scull_c_release(struct inode *inode, struct file *filp){
/* * Nothing to do, because the device is persistent. * A `real' cloned device should be freed on last close */ return 0;}

转载地址:http://lezxi.baihongyu.com/

你可能感兴趣的文章
Multi-Object Tracking with Quadruplet Convolutional Neural Networks
查看>>
关于多目标跟踪的一点理解
查看>>
Learning by tracking:Siamese CNN for robust target association
查看>>
MUSTer:Multi-Store Tracker:A Cognitive Psychology Inspired Approach to Object Tracking
查看>>
Understanding and Diagnosing Visual Tracking Systems
查看>>
Multiple People Tracking by Lifted Multicut and Person Re-identification
查看>>
Visual Tracking Using Attention-Modulated Disintegration and Integration
查看>>
Action-Decision Networks for Visual Tracking with Deep Reinforcement Learning
查看>>
Multiple Object Tracking with High Performance Detection and Appearance Feature
查看>>
深度学习入门(上)-第一章 必备基础知识点
查看>>
ubuntu unzip解压时提示错误 解决方法
查看>>
sprintf函数的说明
查看>>
BOOST_TYPEOF和BOOST_AUTO 作用
查看>>
随机森林概述
查看>>
2011十大战略技术
查看>>
大学应该学的软件知识
查看>>
腾讯与360战争背后的云计算阴影
查看>>
腾讯看了会沉默,360看了会流泪
查看>>
李开复:移动互联网机会最大 微博会现最大赢家
查看>>
2006年的IT十大战略技术
查看>>