Advanced Char Driver Operations [LDD3 06]

本章讲的是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通信。其实和open/close等功能类似,只不过open/close是kernel帮driver实现好的ioctl。

在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和必要的参数。

下面介绍ioctl cmd,这个其实就是个ioctl code,用来标记需要的操作,比如你是要query device的信息,或者需要给device传递数据等,都需要对应不同的cmd。kernel里有现成的utility让driver developer生成合适的cmd,一个cmd主要分成四个部分:type,number,direction,size。

type就是driver自己选的magic number,说白了就是个和kernel不冲突的字符就行,这样可以保证cmd的唯一性;number可以理解为cmd的index,因为一个device driver一般支持很多的ioctl,那么可以用index作为cmd的一部分;direction表示这个ioctl产生的数据流方向,比如读,或者是写,或者既有读又有写。如果是读,就需要kenrel往user mode写memory,如果是写,就是user mode向kernel写memory,这就是两个方向,一共有四种direction,none/读/写/读并且写;size表示传递的数据大小,一般就是sizeof(arg)。

kernel提供的utility function:

_IO(type,nr) (for a command that has no argument)
_IOR(type,nr,datatype) (for reading data from the driver)
_IOW(type,nr,datatype) (for writing data)
_IOWR(type,nr,datatype) (for bidirectional transfers).

同时提供了对应的函数让driver方便的从cmd中获取type/nr/size等:

_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)

关于ioctl的返回值,如果driver不支持,是返回-NOTTY呢,还是返回-EINVAL呢,随便哦。

因为kernel自身支持一些ioctl,并且这些ioctl是在driver的callback被调用之前就会被parse,如果driver定义和kernel一样的cmd,有可能会导致kernel hanle了这个ioctl,driver却一无所知,从而导致user mode拿到了非期望的数据。这些predefined的cmd的type主要有:FIOCLEX, FIONCLEX, FIOASYNC, FIOQSIZE, FIONBIO。

关于ioctl的参数,有两种,int或者pointer,如果是int可以直接使用,如果是pointer,需要注意。这个指针是user mode指针,因此使用之前一定要检查安全性。可以直接使用copy_from_user/copy_to_user来防止使用无效的user mode指针,也可以在使用之前通过access_ok提前检查一下user mode指针的有效性:

int access_ok(int type, const void *addr, unsigned long size);

type就是检查user mode地址addr的读写权限,如果需要读,就设置VERIFY_READ,写或者既读又写就用VERIFY_WRITE,size就是要检查的memory大小。如果access_ok返回true,就可以使用,如果是false,返回错误。

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

put_user(datum, ptr): 如果数据小,使用put_user,而不是copy_to_user,但是使用前需要access_ok检查user mode ptr是否有效。

get_user(local, ptr): 和put_user类似,使用前检查access_ok,确保ptr是有效的user mode ptr。

如果传输的data size超过1/2/4/8bytes,就会报错,必须使用copy_to_user/copy_from_user。

Block I/O

在讲block I/O之前,说一下什么是sleep。sleep就是进程的某种状态,在sleep状态下,进程不会被调度,直到等待的资源可用或者被中断唤醒,否则一直不会被执行。device driver中经常碰到需要sleep的情况,在此之前,有几个原则需要注意:

1, 不要在atomic的context里sleep。比如如果driver拿到了spinlock,seqlock,或者RCU lock,那就不能sleep。

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

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

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

5, 被唤醒时,要再次确认是被你期望的事件唤醒的,因为有可能是被别的信号唤醒的。

如果需要实现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);  //动态初始化

wait的时候需要wait在某个特定的event上,当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。wait_event_timeout和wait_event_interruptible_timeout都有一个timeout值,如果等待超时也会被唤醒。因此如果是可中断的唤醒,在唤醒以后要check返回值,是被signal唤醒还是等待的事件发生。

和sleep相对应的是wake_up:

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

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

Block和No-Block操作

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

关于process sleep的实现细节

第一步先初始化wait_queue_t, 再把它加到wait queue里面。

下一步修改process的运行状态,比如TASK_INTERRUPTIBLE, TASKU_UNINTERRUPTIBLE,分别对应wait_interruptible和wait_unterruptible。

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

最后就是通过调用schedule放弃CPU,让CPU选择其他能运行的进程运行。

Exclusive waits

在wait queue里面,可能有很多的进程在wait,但是很多情况下只有一个进程能获取到资源,其他的进程可能醒来之后做了check,然后又sleep了,如果这样的进程特别多,那么每次wake up都有很多的进程做无用功,这对CPU来说是一种浪费,所以有exclusive wait这种方式,这样的进程被唤醒以后,在它后面的进程就不会被唤醒了。这样的进程都位于wait queue的最后面,wake up被调用的时候,kernel会loop这个wait queue,只要碰到第一个exclusive wait,后面的就不会被唤醒了,而在它前面的进程都不受影响。

关于wake up的细节,就是loop wait queue,然后依次check wait的condition,如果满足条件,就wake up process,这里就不细说了。

关于poll 和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,检测他们状态的变化,如果都不可用,那就block wait。

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

这些bitmask有:POLLIN, POLLRDNORM,POLLRDBAND,POLLPRI,POLLHUP,POLLERR,POLLOUT,POLLWRNORM,POLLWRBAND。如果设备可以读,一般POLLIN | POLLRDNORM会被设上,如果可以写,POLLOUT | POLLWRNORM会被设上。

关于driver中poll的实现,有几个原则需要遵循:

读:

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

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

写:

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

2, 如果output bufer里没有空间,理论上write调用应该被block,直到空间可用;入股设置了O_NONBLOCK,那么就返回-EAGAIN。这种情况下,poll返回0,即不可以写。如果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才会返回。

发布了32 篇原创文章 · 获赞 6 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/scutth/article/details/105307675