Advanced Char Driver Operations [LDD3 06]

16 篇文章 0 订阅
14 篇文章 1 订阅

本章讲的是driver的高级操作,比如:

1, 实现了ioctl,device driver可以满足user mode一些特定的操作。

2, 和user mode做好sync的几种方式。

3, 如何让process进入sleep,以及如何wake up。

4, 非阻塞IO操作,以及读写完成以后如何通知user mode。

ioctl


ioctl可以说device driver肯定会用到的,因为一个device往往提供了不止读写的功能,还可以通过user mode来控制。这部分用来控制的操作,就得用ioctl来实现。ioctl分两部分看,user mode的调用部分和kenel mode的实现部分。

user mode的调用就是一个函数:

int ioctl(int fd, unsigned long cmd, ...);

用户态通过之前open device拿到的fd,加上cmd,以及必要的参数,就可以通过ioctl和kernel driver通信。在kernel mode,ioctl就是一个callback:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

device  driver需要实现这个callback,这样当kernel收到user mode送下来的ioctl,如果是driver自定义的cmd,就会调用driver的ioctl。其中的参数,inode就是open的时候在kernel对应的device file,filp则是open device拿到的file descriptor,cmd 和arg就是送下来的cmd和必要的参数。需要注意的是,如果user mode在调用时没有传递这个arg,那么driver在自己的ioctl callback里收到的arg就是随机值,不要使用。因为ioctl是可变参数的函数,编译器也不会检查到这种错误。

下面介绍ioctl cmd,这个其实就是个ioctl code,用来标记需要的操作,比如你是要query device的信息,或者需要给device传递数据等,都需要对应不同的cmd,这样每一个cmd值都对应了某种操作,方便driver里面来区分。这些cmd需要user mode和kernel mode保持一致,因此这些cmd的定义要share给user mode。

Choosing the ioctl Commands

kernel里有现成的utility让driver developer生成合适的cmd,一个cmd主是由四个域组成的bitmask:type,number,direction,size。

type就是driver自己选的magic number,说白了就是个和kernel不冲突的字符就行,这样可以保证cmd的唯一性,kernel中已经使用了一些magic number并且列在ioctl-number.txt里面,driver需要避免使用这些magic number,magic number占8bit(_IOC_TYPEBITS),driver中应该只使用这一个magic number;number可以理解为cmd的index,因为一个device driver一般支持很多的ioctl,那么可以用index作为cmd的一部分,占8bit(_IOC_NRBITS);direction表示这个ioctl产生的数据流方向,比如读(_IOC_READ),或者是写(_IOC_WRITE),或者既有读又有写(_IOC_READ|_IOC_WRITE),或者不读不写(_IOC_NONE),如果是读,就需要kenrel往user mode写memory,如果是写,就是user mode向kernel写memory;size表示传递的数据大小,一般就是sizeof(arg),占用13或14bit(_IOC_SIZEBITS),架构相关,所以可能不同。

kernel提供的utility function:

_IO(type,nr) //没有参数的ioctl
_IOR(type,nr,datatype) //需要从driver read data的ioctl
_IOW(type,nr,datatype) //需要向driver write data的ioctl
_IOWR(type,nr,datatype) //需要同时读写driver

type和nr都是driver传进来的参数,datatype一般就是sizeof(arg)就可以了。kernel同时提供了对应的函数让driver方便的从cmd中获取type/nr/size等:

_IOC_DIR(nr)  //??
_IOC_TYPE(nr) //获取cmd中的magic number
_IOC_NR(nr)   //获取cmd中的index
_IOC_SIZE(nr) //获取cmd中的arg size

这里有一个例子:

/* Use 'k' as magic number */
#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)

#define SCULL_IOC_MAXNR 14

ioctl的参数,可以是int值,也可以是pointer。

The Return Value

如果user mode传下来不支持的ioctl cmd,driver肯定要返回fail,但是应该返回什么值呢?返回-ENOTTY或者-EINVAL都可以。

The Predefined Commands

因为kernel自身支持一些ioctl,并且这些ioctl是在driver的callback被调用之前就会被parse,如果driver定义和kernel一样的cmd,有可能会导致kernel hanle了这个ioctl,driver不会再收到这个ioctl,从而导致user mode拿到了非期望的数据。

这些predefined的cmd的type主要分成三组:

1, 任何file通用的cmd(普通文件,device,FIFO,socket)

2, 普通文件适用的cmd

3, 文件系统适用的cmd

device driver通常可能需要关心第一种,第一种主要有:FIOCLEX, FIONCLEX, FIOASYNC, FIOQSIZE, FIONBIO。

Using the ioctl Argument

关于ioctl的参数,有两种,int或者pointer,如果是int可以直接使用,如果是pointer,需要注意。

因为这个指针是user mode指针,因此使用之前一定要检查安全性。也可以直接使用copy_from_user/copy_to_user来防止使用无效的user mode指针,使用之前通过access_ok提前检查一下user mode指针的有效性:

#include <asm/uaccess.h>
int access_ok(int type, const void *addr, unsigned long size);

type就是检查user mode地址addr的读写权限,如果需要读,就设置VERIFY_READ,写或者既读又写就用VERIFY_WRITE,addr就是user mode地址,size就是要检查的memory大小(byte)。如果access_ok返回true,就可以使用,如果是false,返回错误-EFAULT。实际上,access_ok大多数情况下只是检查addr是否是user mode地址,仅此而已。而且,大多数driver其实并不需要调用access_ok来检查指针的有效性。

此处有一个例子:

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;

除了使用copy_from_user/copy_to_user以外,kernel还提供了一些函数用来做小数据的copy,比如1/2/4/8个bytes这种。

put_user(datum, ptr)
__put_user(datum, ptr)

 如果数据小,使用put_user,而不是copy_to_user,但是使用前需要access_ok检查user mode ptr是否有效。copy的数据大小取决于ptr指针的类型,如果是char指针,就copy一个byte;如果是unsigned int指针,就copy四个byte,最多可以支持8个byte。put_user会检查memory的访问结果,如果失败会返回fail。__put_user没有检查,因此需要driver自己调用access_ok来检查。

get_user(local, ptr)
__get_user(local, ptr)

 和__put_user类似,使用__get_user前检查access_ok,确保ptr是有效的user mode ptr。使用get_user不用检查,因为返回值说明了一切。

如果传输的data size超过1/2/4/8bytes,就会报错,比如“conversion to non-scalar type requested.”,那么此时必须使用copy_to_user/copy_from_user。

Capabilities and Restricted Operations

对device的访问,需要做权限的检查,一般这个检查在device file打开的时候就检查过了,但是有些特殊的操作,可能需要driver来检查,比如磁带驱动,可以允许读,但是不允许格式化等。

因此,kernel提供了一种capabilities. 它不像root用户,要么拥有很大的权限,可以做一切事情,要么就是普通用户,很多事情都受限。capabilities提供了一种按照分组管理权限的方式,只授予用户或者程序必要的权限,无关的操作不给于权限即可。有两个系统调用可以做这个事情:capget和capset。这些capabilities是kernel固定死的,除非修改kernel的code,否则不能修改。这些capabilities有:CAP_DAC_OVERRIDE,CAP_NET_ADMIN,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_ADMIN,CAP_SYS_TTY_CONFIG。

driver在执行用户态程序的请求之前,需要先检查capability,使用下面的接口:

#include <linux/sched.h>
int capable(int capability);

//sample code
if (! capable (CAP_SYS_ADMIN))
    return -EPERM;

The Implementation of the ioctl Commands

这里就是一个sample:

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))
            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;
    default: /* redundant, as cmd was checked against MAXNR */
        return -ENOTTY;
}
return retval;

//user mode使用
int quantum;

ioctl(fd,SCULL_IOCSQUANTUM, &quantum);/* Set by pointer */
ioctl(fd,SCULL_IOCTQUANTUM, quantum); /* Set by value */

ioctl(fd,SCULL_IOCGQUANTUM, &quantum);/* Get by pointer */
quantum = ioctl(fd,SCULL_IOCQQUANTUM); /* Get by return value */

ioctl(fd,SCULL_IOCXQUANTUM, &quantum); /* Exchange by pointer */
quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); /* Exchange by value */

 

Blocking I/O


很多时候,当user mode调用了ioctl,读写设备数据时,因为读写的buffer不能立即使用,导致user mode请求不能马上满足,这个时候往往需要把进程block住。

Introduction to Sleeping

在讲block I/O之前,说一下什么是sleep。sleep是进程的一种状态,在sleep状态下,进程被从当前可运行的进程列表中移除,并设置为不可调度的状态,因此进程不会被调度,直到等待的资源可用或者被中断唤醒,否则一直不会被执行。

device driver中经常碰到需要sleep的情况,在实现进程sleep时,有几个原则需要注意:

1, 不要在atomic的context里sleep。什么是atomic context?就是一段code,这段code不允许在执行的过程中被打断,从而存在并发运行的可能性。比如driver拿到了spinlock,seqlock,或者RCU lock,那就不能sleep。

2, 如果你关闭了中断,也不能sleep。因为唤醒进程就是靠中断来的,如果中断被关闭,进程就不会被唤醒。

3, 虽然semaphore之类的锁允许持有的时候sleep,但是这个sleep的时间越少也好,因为如果你拿了锁sleep,等待这个锁的线程也会sleep。

4, 如果拿了semaphore,进入sleep,唤醒你的线程也需要拿这个锁,就存在潜在的风险:唤醒线程拿不到锁,就无法把你唤醒。

5, 被唤醒时,要再次确认是被你期望的事件唤醒的,因为在sleep的过程中,这个世界已经变了,也许进程等待的数据已经被别的进程拿走,也有可能进程是被别的信号唤醒的,所以当进程醒来时,要确认自己是被期望的事件唤醒的。

6, 在sleep之前,要确认有人能把你唤醒。否则一旦睡眠,就再也没机会被唤醒了。

如果需要实现sleep和唤醒,可以使用wait_queue,wait_queue中就是一些等待事件发生的process的list,当期望的事件发生时,这些process都会被唤醒。wait_queue的初始化:

DECLARE_WAIT_QUEUE_HEAD(name);  //静态初始化

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);  //动态初始化

Simple Sleeping

wait的时候需要wait在某个特定的queue上,当queue对应的event发生时被唤醒,函数:

 wait_event(queue, condition)
 wait_event_interruptible(queue, condition)
 wait_event_timeout(queue, condition, timeout)
 wait_event_interruptible_timeout(queue, condition, timeout)

上面的几个wait里面,queue就是wait_queue_head_t,注意是按值传递参数。condition是一个表达式,返回true表示被唤醒并继续执行,因为condition中间可能会被调用多次,因此要保证在被调用多次的情况下,condition没有副作用。

上面的函数中,wait_event是不可中断的等待,除非等待的事件发生,否则无法被唤醒,用的较少。wait_event_interruptible是可以被中断唤醒的wait,当返回值是非零值时,说明是被signal唤醒的,此时应该返回-ERESTARTSYS。wait_event_timeout和wait_event_interruptible_timeout都有一个timeout值,如果等待超时也会被唤醒,因此这两个也要check返回值,确保是被期望的事件唤醒。

和sleep相对应的是wake_up:

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

wait_up可以唤醒这个queue上所有等待的进程,wake_up_interruptible只能唤醒可以被中断唤醒的进程。如果你是wait interruptible,那么这两种唤醒方式对你没有区别,通常来说,如果你用wait_event,唤醒就用wake_up,如果你用wait_event_interruptible,唤醒就用wake_up_interruptbile。

此处有一个例子:

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)
{
    printk(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; /* EOF */
}

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; /* succeed, to avoid retrial */
}

这里有一个有意思的地方,sample code的wait contiditon是一个flag值,flag不为零就会被唤醒。如果同时有两个进程等在这个queue上,那么谁会被唤醒呢?如果是在单核CPU上,毫无疑问只有一个进程会被唤醒;如果是多核的CPU,那么这两个进程都会被唤醒,因为这两个进程有可能同时看到非零的flag,后面的flag = 0,有可能谁都机会看到,这种行为有时候是会出现问题的。后面会有解决办法。

Blocking and Nonblocking Operations

取决于open file的时候是否设置了O_NONBLOCK,默认情况下没有设,也就说默认情况下open/read/write都是block的,没有可用资源就会等待。如果设置了这个flag,表示user mode不想等待,所以没有可用资源就立即返回-EAGAIN。

device driver一般都实现了input和output buffer,这样当user mode读写数据时,可以通过buffer作为缓冲,即便device本身比较慢,和user mode的交互也不会因此频繁被block而进入sleep,这样的话就减少了context switch,提高了传输的效率。

这里也有实现block read/write的一个例子:

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 */
};

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;
}

Advanced Sleeping

这里描述了process sleep的实现细节。

How a process sleeps

struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;

如何让一个进程wait在queue里面呢?

第一步先分配并初始化一个wait_queue_t结构体, 再把它加到wait queue里面。这样唤醒的人直到去哪里唤醒等待者。

下一步修改process的运行状态,设置为sleep。进程有很多状态,比如TASK_RUNNING,TASK_INTERRUPTIBLE, TASKU_UNINTERRUPTIBLE,后面两个说明进程正在sleep,分别对应wait_interruptible和wait_uninterruptible。当然,device driver中一般不需要自己来管理进程状态,如果真的需要,可以使用kernel提供的接口:

void set_current_state(int new_state);

再下一步是check一下等待的条件是否已经被满足,因为你在wait之前,有可能已经有人调用了wake up,如果此时不去check,有可能以后再也check不到了。例如这样check的code:

if (!condition)
    schedule( );

只有此时等待的事件仍然没有发生,才通过调用schedule放弃CPU,让CPU选择其他能运行的进程运行。在schedule函数返回时,说明进程已经被某种事件唤醒,并开始执行了,但是如果发现condition已经满足,那么此时要自己再把之前设置的进程状态恢复。

Manual sleeps

上面用的sleep都是kernel实现好的接口,直接调用就可以了。当然,在某些情况下,driver可能需要手动做sleep。

首先,也是要初始化wait queue:

//静态初始化
DEFINE_WAIT(my_wait);
//动态初始化
wait_queue_t my_wait;
init_wait(&my_wait);

下一步就是把wait queue entry添加到wait queue里面去,并且设置进程状态。这两步都是通过下面的接口来做:

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

上面的接口中,queue就是wait queue head,wait是process wait entry,state是进程新的状态,一般是TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。

再调用了prepare_to_wait以后,需要对wait的condition做检查,如果还需要wait,就调用schedule函数。在schedule函数返回时,process已经被唤醒,此时需要调用kernel的另外一个interface:

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

然后再检查wait的condition是否满足,还是process是被signal唤醒的,以及是否还需要再wait。这里有一个例子介绍手动sleep的方法:

/* 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;
}

/* 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;
}

Exclusive waits

在wait queue里面,可能有很多的进程在wait,但是很多情况下只有一个进程能获取到资源,其他的进程可能醒来之后做了check,然后又sleep了。如果这样的进程特别多,每次wake up都有很多的进程做无用功,这对CPU来说是一种浪费,所以有exclusive wait这种方式,保证只有一个进程会被唤醒,别的进程仍然sleep。这样的进程需要设置在自己的wait queue entry里设置WQ_FLAG_EXCLUSIVE这个标记,kernel会把带有这个标记的wait queue entry都放在wait queue的最后面,不带这个标记的wait queue entry放到wait queue的前面。这样,当wake up被调用的时候,kernel会把wait queue的wait entry依次唤醒,直到碰到第一个带有WQ_FLAG_EXCLUSIVE标记的,把它唤醒以后,后面的wait queue entry就不会被唤醒了。

什么时候需要使用WQ_FLAG_EXCLUSIVE呢?考虑下下面两个条件:有可能多个process在wait同一个资源,而当资源产生时,只要一个process醒来就一定可以处理完,没必要唤醒多个process。这种情况下,就需要使用exclusive wait。接口:

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

在prepare wait时使用prepare_to_wait_exclusive,而不是prepare_wait就可以了。注意,wait_event那里是无法修改wait类型的,只能在prepare wait的时候来修改。

The details of waking up

当process被wakeup当时候,最开始执行的是wait queue entry里的function。这个function是kernel的default_wake_up,这个函数会设置process的状态,然后开始执行,必要的时候会做context switch。device driver除非必要,否则不要override kernel的wake up函数。下面driver调用的wake up函数的所有版本:

wake_up(wait_queue_head_t *queue);

wake_up_interruptible(wait_queue_head_t *queue);

wake_up函数会把wait queue里的所有不带exclusive wait的进程全部唤醒,如果存在exclusive wait的进程,唤醒第一个并结束唤醒过程;wait_up_interruptible和wake_up函数类似,区别在于不会唤醒uninterruptible wait的进程。这两个函数在返回之前,有可能会唤醒多个进程并开始调度。

wake_up_nr(wait_queue_head_t *queue, int nr);

wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);

这两个函数和上面的两个功能类似,区别在于,这两个会唤醒nr个exclusive wait的进程,而不只是一个。如果nr是0,代表唤醒所有的exclusive进程。

wake_up_all(wait_queue_head_t *queue);

wake_up_interruptible_all(wait_queue_head_t *queue);

唤醒所有的进程,不考虑是否有exclusive wait。(interruptible版本只唤醒interruptible的wait)

wake_up_interruptible_sync(wait_queue_head_t *queue);

除非调用wake up的人是atomic context,比如拿了spinlock,或者是中断处理程序,否则调用者往往被待唤醒的进程抢占了CPU。有时候不希望这种事发生,调用者就使用这个函数,表示调用者可以被schedule,但是要等我把剩下的一点点事情做完。

 

poll and select


 

使用non-block I/O的程序一般都会是用poll select机制。这种机制包含三个系统调用:poll,epoll,select。

poll,select和epoll都是类似的函数,功能也类似,主要用于用户态application通过nonblock的方式访问fd,也可以是block的方式。poll和select是一样的功能,只是当时有两个不同的组分别做了实现,epoll是poll的扩展版本,可以支持多达上千个fd。在kernel的device driver中,需要实现的都是同一个函数:

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

device driver的poll有两部分组成:

1, 调用poll_wait,等待一个或者多个wait queue,检测他们状态的变化,如果都不能做I/O,那就block wait。

2, 如果有可用的,返回bitmask,表明哪些操作是可用的,并且用non-block的方式实现。

这个接口里,第一个参数是filp就不说了,第二个参数是poll_table结构体,是kernel用来实现poll/epoll/select的。driver并不需要关心里面是什么内容,只需要调用poll_wait,并把这个poll_table传递进去就可以了。kernel之所以把这个结构体传递给driver,是因为需要driver把自己的wait queue添加到poll_table里,当wait queue需要被唤醒时,kernel可以知道。poll_wait接口原型:

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

 

在driver调用了poll_wait之后,还有一个任务是返回bit mask,告诉user mode哪些操作可以不用block就能完成。比如说,device有data到了,那么对于user mode来说read操作可以马上完成不用被block,driver的poll就应该让user mode知道这个状态。

这些bitmask有:

POLLIN

如果non-block的read可用,返回这个flag。 

POLLRDNORM

如果device有normal data可用,返回这个,一般readable的device返回POLLIN | POLLRDNORM。

POLLRDBANDP

out of band的data到达,通知user mode。device driver一般没有这个。

POLLPRI

高优先级的data(out of band)到达,可以non-block的读取。

POLLHUP

如果process看到了file end,driver需要返回这个。

POLLERR

错误条件发生,不懂啥意思。。

POLLOUT

如果设备可以non-block的方式写,设上这个flag。

POLLWRNORM

POLLRDBAND和POLLWRBAND,这两个只能适用于scoket的filp,其他device driver不适用。

这里是poll的一个sample code:

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;
}

 

上面的例子中,只是把inq和outq两个wait queue添加到了wait里面,并且根据具体的状态设置mask。

Interaction with read and write

poll select的目的是让user application提前知道哪些操作可以non-block的方式完成,更重要的是,可以同时wait在多个data stream上。关于driver中poll的实现,有几个原则需要遵循:

Reading data from the device

 1, 只要input buffer有东西,也就是user mode可以读了,那么read调用应当在copy了数据之后立即返回,不等待。尽管有可能copy出去的数据没有read函数期望的那么多。这种情况下,poll返回POLLIN | POLLRDNORM。

2, 如果input buffer里没有东西,理论上read调用应该被block;如果设置了O_NONBLOCK,那么就返回-EAGAIN。这种情况下,poll返回0,即没有数据可用。

3. 如果到达了end of file,read立即返回0,无论有没有设置O_NONBLOCK。poll应该返回POLLHUB。

Writing to the device

1, 只要output buffer里有空间,write调用应该在write了数据之后立即返回,不等待。尽管有可能写进去的数据没有write函数期望的那么多。这种情况下,poll返回POLLOUT | POLLWRNORM。

2, 如果output bufer里没有空间,理论上write调用应该被block,直到空间可用;如果设置了O_NONBLOCK,那么就返回-EAGAIN。这种情况下,poll返回device不可以写。如果device已经满了,不能接受数据,那么write调用可以返回-ENOSPC,无论是否设置O_NONBLOCK。

3, write里的实现,不可以等待data真正传输完成。这里的意思,应该是write调用完后,不要求driver真的把数据写到里device,而应该在fsync这样的callback里实现。device driver一般不实现fsync,所以这里不再讨论。

后面继续讨论了poll背后的实现,涉及到kernel是如何让user mode的poll/select/epoll调用等待在对应的wait queue上。其实也简单,user mode进到kernel以后,kernel根据要等待的fd个数,创建poll_table_entry,有多少个fd就有多少个entry,这些entry最后都被封装在poll_table_struct里,被传递给每一个device driver的poll。而每一个entry里,其实就是一个wait queue,当这些wait queue都返回时,poll/select/epoll才会返回。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值