深入linux设备驱动程序内核机制(第七章) 读书笔记

时间:2022-12-23 17:54:51
第七章 设备文件的高级操作


      本文欢迎转载, 请标明出处.

       本文出处:http://blog.csdn.net/dyron


7.1 ioctl文件操作

    7.1.1 ioctl的系统调用

    用户空间ioctl的原型为: int ioctl(int fd, int request, ...);
    驱动空间ioctl的原型为:

    struct file_operations {    ...
long (*unlocked_ioctl)(struct file *, unsinged int, unsigned long);
int (*ioctl)(struct inode *, struct file *, unsinged int, unsigned long);
...
}
    驱动程序应该实现unlocked_ioctl, ioctl是为了向后兼容的代理。

    用户空间程序调用ioctl函数时, 系统进入sys_ioctl进入内核空间。
    SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)    {
struct file *filp;
int error = -EBADF;
int fput_needed;

filp = fget_light(fd, &fput_needed);
if (!filp)
goto out;

error = security_file_ioctl(filp, cmd, arg);
if (error)
goto out_fput;

error = do_vfs_ioctl(filp, fd, cmd, arg);
out_fput:
fput_light(filp, fput_needed);
out:
return error;
}
    还记得第2章说的fd与filp的关联吧, 当用户空间打开一个设备时, 内核将为之分本fd, 同时生成filp, fd
    与filp通过当前进程的files管理的文件描述符表关联起来。系统打开文件时获得的fd,将作为参数传给ioctl
    函数。 在这里第1个参数是fd, , 第2个是filp, 这里的fget_light就是通过fd获得filp, 接下来调用do_vfs
    _ioctl.
    int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,        unsigned long arg)
{
int error = 0;
int __user *argp = (int __user *)arg;
struct inode *inode = filp->f_path.dentry->d_inode;

switch (cmd) {
case FIOCLEX:
set_close_on_exec(fd, 1);
break;

case FIONCLEX:
set_close_on_exec(fd, 0);
break;

case FIONBIO:
error = ioctl_fionbio(filp, argp);
break;

case FIOASYNC:
error = ioctl_fioasync(fd, filp, argp);
break;

case FIOQSIZE:
if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) ||
S_ISLNK(inode->i_mode)) {
loff_t res = inode_get_bytes(inode);
error = copy_to_user(argp, &res, sizeof(res)) ?
-EFAULT : 0;
} else
error = -ENOTTY;
break;

case FIFREEZE:
error = ioctl_fsfreeze(filp);
break;

case FITHAW:
error = ioctl_fsthaw(filp);
break;

case FS_IOC_FIEMAP:
return ioctl_fiemap(filp, arg);

case FIGETBSZ:
return put_user(inode->i_sb->s_blocksize, argp);

default:
if (S_ISREG(inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);
break;
}
return error;
}
    可以看出ioctl函数原形中的2,3参数用户对应do_vfs_ioctl函数中的cmd, arg. 有过ioctl经验的读者应该知
    道, 最终会调用到do_vfs_ioctl中default分支下的vfs_ioctl函数。 最终会调用filp->f_op->ioctl中去。

    内核在调用filp->f_op->ioctl时, 使用了lock_kernel和unlock_kernel作为互斥手段, lock_kernel和unl
    ock_kernel是一种粗粒度的大内核锁BKL(big kernel lock), 这种全局范围的锁对于细粒度的锁而言, 明显
    地降低系统性能。 虽然内核中有这种所, 但应该避免使用, 所以应该使用unlocked_ioctl, 它们已经脱离
    了大内核锁的保护, 因此驱动程序在实现unlocked_ioctl函数时, 应该使用自己的互斥机制。

    7.1.2 ioctl的命令编码

    ioctl用来在用户空间和驱动模块间传递控制信息, 这个信息主要以cmd和arg的形式存在, 因此需要建立一
   套规则来构造和解析cmd参数的组成, 确保任何一个遵循这些规则编码出来的cmd在系统范围内是唯一的。

    为构造ioctl的cmd参数, 内核使用一个32位无符号整数并将其分成四个部分.

    31    29                16 15           8 7        0
   |DIR   |        SIZE       |     TYPE     |    NR   |

    NR为功能号, 长度为8位
    TYPE为一ASCII字符, 长度为8位, 假定对每个驱动都是唯一的, 常常含有MAGIC字样。
    SIZE表示ioctl中arg参数的大小, 长度与体系结构相关,通常是14位。
    DIR表示cmd的类型:read, write, read-write, 长度为2位. 用于表示数据传输的方向。该字段定义的宏有:
    _IOC_NONE, 表示在ioctl过程中,用户空间向内核写数据;_IOC_READ, 表示用户空间从内核空间读数据;_IOC_
    WRITE|_IOC_READ, 表示参数数据在用户空间和内核空间双向传递.

    内核用宏_IOC将NR, TYPE, SIZE, DIR构造cmd参数:

    #define _IOC(dir, type, nr, size) \    (((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))

#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
    常见的预定义有:
    FIOCLEX: 执行时关闭, 通知内核在调用进程执行一个新程序时, 自动关闭打开的文件.    FIONCLEX: 清除执行时关闭标志, 清除由FIOCLEX命令设置的标志.    FIONBIO: 文件的ioctl为非阻塞型I/O操作, 修改在flip->f_flags的O_NONBLOCK标志.????????既然修改了这个f_flags标志, 那么在read/write时也会变成非阻塞操作吧.    FIOASYNC: 设置或复位文件的异步通知, 实际执行者是fcntl, 内核并不使用该cmd.    FIOQSIZE: 获得一个文件都者目录的大小, 用于设备文件时,返回ENOTTY.
    7.1.3 copy_from_user和copy_to_user
    static inline long copy_from_user(void *to,        const void __user * from, unsigned long n)    {    might_sleep();    if (access_ok(VERIFY_READ, from, n))        return __copy_from_user(to, from, n);    else        return n;    }   
    *to是内核空间指针, *from是用户空间指针, n从用户空间向内核空间拷贝数据的字节数.如果完成拷贝,返回
    0, 否则返回没有完成拷贝的字节数.

    might_sleep在定义了CONFIG_PREEMPT_VOLUNTARY的情况下会形成一个显示的抢占调度点,也就是说might_sle
    ep会可能自动放弃cpu, copy_from_user有可能让当前进程进入睡眠状态.

    access_ok用来对用户窨的地址from做有效性检验. 实现和体系架构相关.
    #define access_ok(type, addr, size) (__range_ok(addr, size) == 0)

    access_ok中的每一个参数type没有用到, __range_ok用来判断addr+size后是否还在进程的用户空间范围内.

    在内核态, 如果程序试图访问一个尚未被提交物理页面的用户空间地址, 内核必须对此保持警惕而不能象用
    户空间那样毫无察觉.

    copy_from_user和memcpy的区别在于, 如果发生缺页异常时, copy_form_user会定义一个__ex_table section
    , 而memcpy则不会, 极可能会出现:

    BUG: unable to handle kernel paging request at 188be008

    从内核拷贝数据到用户空间使用: copy_to_user.

    除了上述的两个函数, 用户空间和内核空间交换数据时还有两个函数: get_user和put_user. 这些函数主要完
    成一些简单类型变量的拷贝任务, 函数内部将对ptr指向的对象长度进行检查, 大部分平台只支持长度为1, 2,
    4的变量.

    #define get_user(x, ptr)                                        \    ({                                                              \
might_sleep(); \
access_ok(VERIFY_READ, ptr, sizeof(*ptr)) ? \
__get_user(x, ptr) : \
-EFAULT; \
})
    get_user将用户窨ptr指向的数据拷凡到内核窨的变量x中, 虽然内部实现多为单条汇编指令实现的内存操作,
    但依然会有".fixup"和"__ex_table" section. 函数成功返回0, 否则返回-EFAULT.
    #define put_user(x, ptr)                                        \    ({                                                              \
might_sleep(); \
access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)) ? \
__put_user(x, ptr) : \
-EFAULT; \
})
    put_user用来将内核空间的一个简单类型x拷贝到ptr指向的用户空间中, 函数能自动判断变量的类型,成功返
    回0, 否则返回-EFAULT.

    ioctl的实现中, 一般先用_IOC_TYPE对cmd进行初步的检验, 之后调用access_ok验证用户空间指针的有效性,
    因为access_ok在ioctl开始就被调用, 所以之后用的__put_user, __get_user, __copy_from_user这种形式,
    否则应该用put_user, get_user, copy_from_user. 另外因为驱动是unlocked_ioctl, 所以驱动需要实现自忆
    的互斥机制.

7.2 字符设备的I/O模型

   x拷贝到ptr指向的用户空间中, 函数能自动判断变量的类型,成功返
  回0, 否则返回-EFAULT.

 ioctl的实现中, 一般先用_IOC_TYPE对cmd进行初步的检验, 之后调用access_ok验证用户空间指针的有效性,
因为access_ok在ioctl开始就被调用, 所以之后用的__put_user, __get_user, __copy_from_user这种形式,
   否则应该用put_user, get_user, copy_from_user. 另外因为驱动是unlocked_ioctl, 所以驱动需要实现自忆
  的互斥机制.

7.2 字符设备的I/O模型

    . 同步阻塞I/O
    对于同步阻塞I/O, 应用程序执行一个系统调用对设备进行read/write, 这种操作会阻塞应用直到设备完成
    read/write操作或者返回一个错误码. 在阻塞的这段时间里, 程序代表的进程并不消耗CPU时间, 为了支持
    设备的这种I/O模式, 设备驱动需要实现read/write方法.

    . 同步非阻塞I/O
    设备文件以非阻塞形式打开O_NONBLOCK, 如果设备不能立即完成用户请求的I/O操作, 立刻返回一个错误码,
    EAGAIN/EWOULDBLOCK.

    . 异步阻塞I/O
    这种模式的I/O操作并不是阻塞在设备的读写操作本身, 而是阻塞在某一组设备文件描述符上, 当其中某此描
    述符代表的设备对读写操作已就绪时, 阻塞状态将解除, 用户程序随后可以对这些设备进行读写操作. linux
    的字符设备驱动程序需要实现poll方法.

    . 异步非阻塞I/O
    这种模式下, 读写操作会立即返回, 用户程序的读写请求将放入一个请求队列中, 由设备在后台异步完成,当
    设备完成了本次的读写操作, 将通过信号或者回调函数的方式通知用户程序. 在linux中, 块设备和网络设备
    的I/O模型属于异步非阻塞型, 对于字符设备来说, 极少有驱动程序需要去实现这种模式的I/O操作.

7.3 同步阻塞型I/O

    7.3.1 wait_event_interruptible

    宏wait_event_interruptible用来将当前调用它的进程睡眠等待在一个event上, 直到进程被唤醒并且需要的
    condition条件为真. 睡眠进程的状态是TASK_INTERRUPTIBLE, 意味着它可以被用户程序所中断而结束.
    #define wait_event_interruptible(wq, condition)                         \    ({                                                                      \
int __ret = 0; \
if (!(condition)) \
__wait_event_interruptible(wq, condition, __ret); \
__ret; \
})
    wait_event_inetrruptible在condition不为真是将睡眠在等待队列wq上, 所以函数首先判断condition是否为
    真,为真则直接返回, 否则调用它的进程通过__wait_event_interruptible进入睡眠.

    #define __wait_event_interruptible_timeout(wq, condition, ret)          \        do {                                                                    \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
ret = schedule_timeout(ret); \
if (!ret) \
break; \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
finish_wait(&wq, &__wait); \
} while (0)
    DEFINE_WAIT(__wait)用来定义一个名为"__wait"的等待队列节点.

    wait_queue_t __wait = {    .private = current,    .func     = autoremove_wake_function,    .task_list = LIST_HEAD_INIT((__wait).task_list),    };
    __wait中的autoremove_wake_function在节点上的进程被唤醒时调用, private指向当前调用wait_event_int
    errupt的进程.

    在函数的for循环中, 首先调用prepare_to_wait来完成睡眠前的准备工作, 具实任务是:
    1. 清除__wait节点flags 中的WQ_FLAG_EXCLUSIVE标志,该樗在唤醒函数中用到:__wait->flags&=~WQ_FLAG_E
    XCLUSIVE;
    2. 将__wait节点加入到wq中:__add_wait_queue(q,wait),加入__wait节点到等待队列中成为头节点后的第一
    个节点, 所以后进来的进程将最先被唤醒.
    3. 将当前进行状态设置为TASK_INTERRUPTIBLE.

    prepare_to_wait之后进程依然在调度哭的运行队列中,之后如果condition条件依然为假并且当前进程也没有
    等待的信号要处理, schedule将被调用,然后调度哭将把当前进程从它的运行队列中移除.

    进程被唤醒时, schedule函数返回, 通过continue继续for循环直到condition为真时, 才通过break进入到fi
    nish_wait, 后者基本是prepare_to_wait的一个反向操作: 重新设置进程状态为TASK_RUNNING, 然后将__wait
    节点从wq中删除, 如果休眠进程被某个信号所中断, 将返回-ERESTARTSYS.

    wait_event:
    函数进程进入等待队列, 睡眠状态为TASK_UNINTERRUPTIBLE. 与wait_event_interruptible的区别是, 它使睡
    眠进程不可被中断, 且当唤醒时也不会检查是否有等待信号需要处理.

    wait_event_timeout:
    如果调用进程进入睡眠, 状态将也是TASK_UNINTERRUPTIBLE, 意味着不可被中断, 唤醒时也不检查等待的信号
    是否需要处理. 它会有一个时间期限, 时间到达到将返回0.

    wait_event_interruptible_timeout:
    在wait_event_interruptible的基础上加了时间期限, 指定时间到达时函数将返回0.

    7.3.2 wake_up_interruptible

    #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL) 
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;

spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;

list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;

if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
    wake_up_interruptible(x)展开后实际调用__wake_up_common(x, TASK_INTERRUPTIBLE, 1, 0, NULL), 后者
    通过list_for_each_entry_safe对等待队列x进行遍历,对于遍历过程中的每个等待节点, 都会调用该节点上的
    函数func, 由前面可知, func指向 autoremove_wake_function, 其主要功能是唤醒当前等待节点并从等待队
    列移除, 通常情况会返回成功1. 如果想让函数结束遍历,必须满足三个条件:

    1. 负责唤醒进程的函数func成功返回;
    2. 等待节点的flags设置了WQ_FLAG_EXCLUSIVE这个排他性标志. 如果设置了, 唤醒当前进程后不再继续.
    3. nr_exclusive等于1, nr_exclusive表示允许唤醒的排他性进程的数量.

    对于条件1,通常都会满足, 如果不满足, 极大的可能性是因为唤醒时使用的进程状态标志不对. 这里可以将函
    数结束继续唤醒队列中进程的条件归纳为: 遇到一个排他性唤醒的节点并且当前允许排他性唤醒的进程数为1.

    因为在wait_event_interruptible调用中, WQ_FLAG_EXCLUSIVE标志是被清除的, 这意味着wake_up_interrut
    ible会试图唤醒等待队列x每个节点上的进程.

    在唤醒时, 将调用autoremove_wake_function函数, 进而调用try_to_wake_up 函数, 在这个函数里用p->
    state & state将wake_up系列函数中的进程状态与要唤醒的进程状态进行检查, 如果p->state & state = 0,
    那么唤醒操作返回0, 不成功. 因此wake_up_interruptible只能唤醒通过wait_event_interruptible睡眠的
    进程.
    #define wake_up(x)                      __wake_up(x, TASK_NORMAL, 1, NULL)    #define wake_up_nr(x, nr)               __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL)

#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible_sync(x) __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)
    因为TASK_NORMAL在内核中定义为(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE, 所以wake_up可以取代wake
    _up_interruptible, 也可以用来唤醒因wait_event而睡眠的进程.

    wake_up_nr和wake_up_all表示可以唤醒的排他性进程的数量. wake_up_nr可以唤醒nr个, wake_up_all可以唤
    醒队列中所有的排他性进程, wake_up则只能唤醒一个.
    wake_up_interruptible除了只能唤醒TASK_INTERRUPTIBLE状态的进程外, 其它的功能和wake_up一样.

    wake_up_locked和wake_up的唯一区别是: wait_up内部分使用等待队列的自旋锁, wake_up_locked则不会,所
    以wake_up_locked的使用者, 必须自己考虑加锁的问题. wake_up_interruptible_sync用来保证调用它的进程
    不会被唤醒的进程所抢占崦调出处理哭.

    驱动中利用它实现阻塞的I/O操作步骤如下:
    1. 首先定义一个等待队列头, 可以用DECLARE_WAIT_QUEUE_HEAD宏来静态定义.
    static DECLARE_WAIT_QUEUE_HEAD(demo_rd_wq);
    也可以动态初始化一个头节点, 用init_waitqueue_head.
    static wait_queue_head_t demo_rd_wq;
    init_waitqueue_head(&demo_rd_wq);

    2. 在阻塞I/O函数的实现中调用wait_event_interruptible等待数据可用
    wait_event_interruptible(wq, test_bit(RD_DATA_READY, &demodev_buf->state));

    3. 实现一个demo_read等待的数据可用时唤醒操作
    set_bit(RD_DATA_READY, &demodev_buf->state);
    wake_up_interruptible(demo_rd_wq);

7.4 同步非阻塞型I/O

    驱动程序的read/write应该在它的执行流程中检测filp->f_flags上的O_NONBLOCK位有没有被设置, 如果设置
    了的话, 操作可以简单地返回一个错误码-EAGAIN.

7.5 异步阻塞型I/O

    要实现异步阻塞型I/O, 驱动要在它的file_operations结构中实现自己的poll函数
    unsigned int (*poll)(struct file *, struct poll_table_struct *);

    应用程序的poll函数将通过sys_poll进入内核空间:
        int sys_poll(struct pollfd __user *ufds, unsigned int nfds, long timeout_msecs);

    系统调用的原型和用户空间的poll一样, 函数内部使用do_sys_poll来实现其核心功能:
    int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec *end_time)    {
...
poll_initwait(&table);
fdcount = do_poll(nfsd, head, &table, end_time);
poll_freewait(&table);
...
}
    其中table变量是关键元素, 变量中的成员poll_table pt将被传给驱动. table是个struct poll_wqueues类型
    的变量, 定义为:
    struct poll_wqueues {    poll_table pt;
struct poll_table_page *table;
struct task_struct *polling_task;
int triggered;
int error;
int inline_index;
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};

void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}
    poll_initwait除了初始化poll_wqueues中的poll_table成员pt, 另一个重要的步骤是把当前进程的task_str
    uct对象指针current放入到pwq的polling_task中.

    poll_freewait调用只做一些资源释放类的辅助工作. do_sys_poll的核心实现是do_poll, 该函数可能会被阻
    塞, 当其返回时, 返回值fdcount将传递到用户空间以指标本次操作的状态.

    . fdcount > 0, 表明集合中有fdcount个文件描述符可以进行读/写.
    . fdcount = 0, 表明集合中所有文件描述符尚无状态变化, timeout指定的时间到,超时.
    . fdcount < 0, 表明函数调用失败, 错误原因将写入errno.

    do_poll函数的框架结构如下:
    其核心是for循环, 循环中首先对文件描述符集合中的每个描述符调用do_pollfd, 同时传入一个struct poll
    fd类型的指针.
    struct pollfd {    int fd;
short events;
short revents;
}
    do_pollfd的主要功能是根据当前的fd, 找到对应的struct file *filp对象, 然后调用poll流程.
    fd = pollfd->fd;    file = fget_light(fd, &fput_needed);
mask = file->f_op->poll(file, pwait);
pollfd->revents = mask;
    mask由设备驱动中的poll返回, 用来记录驱动中发生的事件, 然后do_pollfd将其记录在pollfd->revents中,
    并最终返回到用户空间.

    根据do_poll框架可知:
    首先每个fd所对应的驱动程序在自己实现的poll例程中不应该睡眠, 应用程序调用的poll只会睡眠在pool_sc
    hedule_timeout这里; 其次如果不考虑超时因素, 当前进程从poll_schedule_timeout中醒来应该是由驱动程
    度中的poll所致, 因为只有驱动才知道自己管理的设备什么时候数据就绪. 所以驱动就少不了唤醒环节.

    需要注意的是select的用户进程是以TASK_INTERRUPTIBLE状态进入睡眠的, 可被中断的睡眠状态.

    所以我们可以总结出poll特性支持实际上只分成两部分:
    1. 是poll例程本向, 它将某一等待节点对象加入到自己管理的等待队列中.
    2. 数据就绪后的唤醒操作.

    在每个fd调用do_pollfd(pfd, pt)时都会传入一个pt参数.
    poll_table *pt = &wait->pt;

    pt是个pool_table型的指针, 驱动中pool例程的关键调用是pool_wait;
    static inline void poll_wait(struct file *filp, wait_queue_head_t *wait_address, pool_table *p)    {
if (p && wait_address)
__pollwait(filp, wait_address, p);
}

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct pool_wqueues, pt);
struct poll_table_entry *entry = poll_get_entry(pwq);

if (!entry)
return;

get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
entry->key = p->key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait);
}
    函数的大意为产生一个等待节点entry->wait, 该节点上的唤醒函数为pollwake, 然后加入到等待队列wait_a
    ddress中, 后者是由各自驱动维护的等待队列. 这里对poll_table指针p的作用是, 通过p获得pwq指针, 然后
    从pwq管理的数据结构上获得等待节点所在的窨.

    对于一个驱动而言, 为了实现自己的poll例程, 需要构造自己的等待队列, 然后通过poll_wait将一个等待节
    点加入到自己的等待队列中, poll_wait负责从pwq对象中申请容纳等待结点的空间并初始化, 其中唤醒函数为
    pollwake. 等待结点的对象来自内核, 因此内核随后可以在poll_freewait中将这些节点清除掉.

    当驱动的中断是发现自己的设备数据可以使用, 驱动将在自己管理的等待队列上调用wake_up函数, 这将导致
    等待节点中的pollwake被调用.
    static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)    {
struct poll_table_entry *entry;

entry = container_of(wait, struct poll_table_entry, wait);
if (key && !((unsigned long)key & entry->key))
return 0;
return __pollwake(wait, mode, sync, key);
}
    唤醒操作发生在__pollwake中, 它将调用try_to_wake_up去唤醒进程, __pollwake中只需要找到要唤醒进程的
    task_struct对象的指针

    struct poll_wqueues *pwq = wait->private;    task_struct *p = pwq->polling_task;
try_to_wake_up(p...);
    最后一句try_to_wake_up将试图唤醒睡眠在poll_schedule_timeout上的进程, 将导致用户从select调用中返
    回.

    驱动程序的poll除了报告设备的数据是否就绪外, 还报告是哪种情况就绪, 读或者写或者其它.
    #define POLLIN          0x0001  //非高优先级的数据(out-of_band),可以被无阻塞地读取.    #define POLLPRI         0x0002  //高优先级数据可以被无阻塞的读取.
#define POLLOUT 0x0004 //数据可以无阻塞的写入.
#define POLLERR 0x0008 //设备发生了错误.
#define POLLHUP 0x0010 //与设备的链接已经断开
#define POLLNVAL 0x0020

/* The rest seem to be more-or-less nonstandard. Check them! */
#define POLLRDNORM 0x0040 //正常数据可以无阻塞的读取.
#define POLLRDBAND 0x0080
#ifndef POLLWRNORM
#define POLLWRNORM 0x0100 //正常数据可以无阻塞的写入.
7.6 异步非阻塞型I/O

    驱动中如果需要支持异步非阻塞型I/O, 需要实现aio_read/aio_write方法:
    ssize_t (*aio_read)(struct kiocb *, const struct iovec *, unsigned long loff_t);    ssize_t (*aio_write)(struct kiocb *, const struct iovec *, unsigned long, loff_t);
    第1个参数是struct kiocb类型的指针, 用来封装一个读写请求的完整上下文.

    struct kiocb中的成员大体上可分成三部分:
    1. 文件相关的ki_filp;
    2. 与发出异步I/O请求的进程相关的ki_obj;
    3. 与当前I/O请求本身相关数据存储空间,字节数与偏移量等一些成员.

    用日元窨使用异步I/O有两种方式:
    1. 使用异步I/O的API函数, 如aio_read, aio_write, aio_error等.
    2. 使用linux的系统调用, 如io_setup, io_submit, io_destroy等.

7.7 驱动程序的fsync例程

    fsync用来同步设备的写入操作, 将数据真正的写入到设备中去.

7.8 fasync例程

    int (*fasync)(int, struct file *, int);

    poll, select和epoll在与设备沟数据是否就绪是, 采用的是轮询的方式, 其实除了轮询的方式, 还有一种类
    似于中断的方式, 当设备中的数据就绪是, 驱动会给应用程序发送一个信号. 这种模式就是fasync.

    执行这个操作, 应该程序需要做两件事:
    1. 通过fcntl的F_SETOWN命令将进各的ID告诉驱动, 这样当驱动数据就绪时才知道要通知哪个进程.
    2. 通过fcntl的F_SETFL命令设备FASYNC标志, 让驱动启动异步通知机制.

    fcntl通过系统调用sys_fcntl与内核交互, 后者的核心调用是do_fcntl:
    F_SETFL和F_SETOWN比较直白, f_setown将要通知进程的ID相关信息记录在filp->f_owner中, setfl直接调用
    了驱动提供的fasync例程.

    驱动程序在其fasync例程中需要fasync_helper和kill_fasync两个函数. 前者将当前要通知的进程加入一个链
    表或从链表中移除, 取决于应用调用fcntl时是否设置了FASYNC. kill_fasync在设备某一事件发生时负责通知
    链表中所有相关进程.

    int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)    {
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}
    对于int on, 在setfl函数中, 传给它的条件表达式是(arg &FASYNC)!=0, 如果应用程序在调用fcntl时, 对
    F_SETFL使用参数设置了FASYNC, 那么这里结果为1, on就为1, 表明应用程序正在启动异步通知机制, 反之
    正在清除FASYNC标志, 将关闭驱动的fasync异步通知特性.

    fasync_helper的主要功能是维护一个需要通知的进程链表fapp, 如果应用需要获得异步通知的通力, 需要通
    过fcntl的F_SETFL命令设置FASYNC标志, 如果设置了fasync标志, 驱动的fasync在调用fasync_helper时将用
    fasync_add_entry将需要通知的进程加入驱动维护的链表中, 否则就用fasync_remove_entry将其移除.

    驱动为实现fasync例程, 需要维护一个struct fasync_struct的链表, 链表中的每个节点对象代表着一个需要
    通知的进程, 进程的ID信息存在节点对象的fa_file->f_owner中. 增/更新结点为的操作由fasync_add_entry
    完成, 删除由fasync_remove_entry完成.

    如果条件满足时, 驱动需要向fasync链表中的每个等待通知的进程发送信号. 内核为此提供了一个kill-fasy
    nc函数.
    void kill_fasync(struct fasync_struct **fp, int sig, int band)    {
/* First a quick test without locking: usually
* the list is empty.
*/
if (*fp) {
rcu_read_lock();
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
rcu_read_unlock();
}
}
    其实质的操作发生在kill_fasync_rcu中, 思想很明确, 通过while循环遍历fasync链表, 对每个进程调用send
    _sigio来向其发送SIGIO信号通知进程.

    总结一个对fasync的支持, 首先驱动需要定义一个struct fasync_struct指针, 当用户程序调用fcntl用F_SE
    TFL命令来设置或清除FASYNC标志时, 驱动应在其fasync例程中调用内核提供的fasync_helper函数在struct
    fasync_struct 指针所指向的链表中增删一个节点, 每个节点代表一个进程. 其次, 当进程所需要的数据就绪
    时, 驱动负责向其维护的fasync_struct链表中的每个进程发信号, 内核提供的kill_fasync来完成.

7.9 llseek例程

 
   loff_t (*llseek)(struct file *, loff_t, int);
SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, origin)
{
off_t retval;
struct file * file;
int fput_needed;

retval = -EBADF;
file = fget_light(fd, &fput_needed);
if (!file)
goto bad;

retval = -EINVAL;
if (origin <= SEEK_MAX) {
loff_t res = vfs_llseek(file, offset, origin);
retval = res;
if (res != (loff_t)retval)
retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
}
fput_light(file, fput_needed);
bad:
return retval;
}
    需要关注的一个是if(origin <= SEEK_MAX), 应用调用lseek时, origin参数只有三个选择
    #define SEEK_SET        0       /* seek relative to beginning of file */    #define SEEK_CUR        1       /* seek relative to current file position */    #define SEEK_END        2       /* seek relative to end of file */    #define SEEK_MAX        SEEK_END   
    所以这里检是就是确保origin的有效性. 另一个需要注意的地方是检查完origin参数后调用vfs_llseek.

    loff_t vfs_llseek(struct file *file, loff_t offset, int origin)    {    loff_t (*fn)(struct file *, loff_t, int);    fn = no_llseek;    if (file->f_mode & FMODE_LSEEK) {        if (file->f_op && file->f_op->llseek)        fn = file->f_op->llseek;    }    return fn(file, offset, origin);    }
    这里需要关注函数指针fn的赋值, fn有三个可能的值: no_llseek, default_llseek和驱动提供的llseek.
    如果file->f_mode中的FMODE_LSEEK标志没有设置, 那么lseek最终调用的是no_llseek, 该函数直接返回一个
    错误码-ESPIPE. 设备文件上的open默认是设置FMODE_LSEEK的, 所以驱动提供了llseek,就会调用它, 否则将
    调用默认的default_llseek, 该函数通过修改filp->f_pos来达到定位文件的目的.

    如果调用到了驱动的llseek, 驱动需要根据用户传入的偏移值off和调整的起始位置参数来决定如何定位文件
    ,实际上有些设备的定位是没有意义的, 所以驱动不应该提供llseek, 同时也不希望使用内核提供的默认的de
    fault_llseek,  可以在打开这类设备时调用nonseekablk_open来关闭FMODE_LSEEK标志.

    int nonseekable_open(struct inode *inode, struct file *filp)    {
filp->f_mode &= ~(FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE);
return 0;
}
7.10 访问权能

    bool capable(int cap)    {    return ns_capable(&init_user_ns, cap);    }
...........内核更新真是快啊, 就在这一章就有好多函数不一样了.

    参数cap用来指定对当前进程进行检查的权能数值, 内核*定义了33个权能数值. 函数cap_valid用来检查
    cap所标球的权能是否在内核事先定义的权限范围内.

    真正的权能权能检查是在security_capable中. 如果进程具有指定的权能, 将返回0, 返回一直错误码-EPERM.
    当驱动不具有进一步的操作权限时, 常常返回一个错误码-EPERM.

7.11 本章小结

      本文欢迎转载, 请标明出处.

       本文出处:http://blog.csdn.net/dyron


     本章讨论了struct file_operations操作中的大部分函数实现, 其中有ioctl. poll, fasync.
     poll可以让应用通过select等API睡眠等待在一组fd上, 当前的驱动所在的设备节点就对应其中的一个fd,
     进程醒来的条件是, 或者时间到期, 或者fd_set中的某一个fd就绪. 字符设备在实现自己的poll例程时, 需
     要维护一个自己的等待队列, 将来自内核的等待节点通过poll_wait加入到自己的等待队列上, 当数据就绪
     时唤醒等待队列上的进程, 这使得应用的select函数返回.

     fasync用来实现一个异步通知机制, 用户程序通过fcntl来向驱动表明是否希望在某一事件出现时得到通知.
     驱动实现fasync例程时主要信赖两个内核提供的函数:fasync_helper和kill_fasync, 前者将需要通知的进
     程加入一个链表, 后者在应用程序关注的事件发生时通过信号发送的方式来通知应用程序.