LDD3 read notes Chapter 6
时间:2009-07-09 来源:hylpro
2009.6.x Chapter 6 Advanced Char Driver Operations Ioctl int ioctl(int fd, unsigned long cmd, ...); /*user space 原型*/ * ... 代表一个可选参数 char *argp . ... 只是为了防止编译的类型检查 *每个ioctl 命令类似一个没有列出的系统调用, 因为可以传递指针,所以可以交换任意数据 int (*ioctl) (struct inode *inode, struct file *filp,unsigned int cmd, unsigned long arg); /*kernel space 原型*/ * inode, filp 对应于应用程序open得到的fd * 如果user space没有传递arg, 则访问arg后果不确定 选择ioctl命令字 * 整个系统的ioctl 命令子唯一, 这样可以避免一些误操作, 设备不对的话, 返回EINVAL 而不是进行一些奇怪的操作 *include/asm/ioctl.h and Documentation/ioctl-number.txt * 老的cmd 用16bit, ,高8bit是设备相关的magic number, 低8bit是设备唯一的值 *新的bit 域定义在<linux/ioctl.h>, 分成4个bit域 type: 查询ioctl-number.txt, 选择未用值, 8bit, _IOC_TYPEBITS number: 命令序列号, 8bit, _IOC_NRBITS direction: _IOC_NONE (no data transfer), _IOC_READ, _IOC_WRITE,and _IOC_READ|_IOC_WRITE 方向相对于user space来讲的. size: user sapce 数据大小, 13, 或者14bit, _IOC_SIZEBITS. 可以不适用size域, kernel 也不检查. 用上可以做到向 后兼容, 万一格式改变可以看size的大小得到用的哪个版本. *<linux/ioctl.h> 包含 <asm/ioctl.h> 有些有用的宏定义 _IO(type,nr) : 没有参数的宏 _IOR(type,nr,datatype): read from driver , datatype传递相应数据结构,如 _IOW(SCULL_IOC_MAGIC, 2, int) _IOW(type,nr,datatype): write to driver _IOWR(type,nr,datatype): 双向传输 _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr), and _IOC_SIZE(nr). *可以用返回给user spce 正值的方式, 交换简单数据 (负值代表系统调用出错) *number 域可以R,W 共用一个, 因为方向值不同 * 除了几个特殊的cmd, kernel 是不关心这个值的, 一个老的用法例子是< linux/kd.h> * 对于不存在的cmd, 可以返回-EINVAL, 虽然posix 标准是返回-ENOTTY. kernel 预定以的命令 分成三种, (你的driver 不会得到这样的请求, 会被kernel 截住, 所以不要用这种命令字) • Those that can be issued on any file (regular, device, FIFO, or socket) magic number 'T' • Those that are issued only on regular files • Those specific to the filesystem type 一个driver 只需关注第一种, 列出如下: FIOCLEX : set Set the close-on-exec flag FIONCLEX: Clear the close-on-exec flag FIOASYNC :Set or reset asynchronous notification for the file 一直到2.2.24的时候 kernel 一直不怎么正确的适用这个标记,来修改 O_SYNC flag.实际让没人用, fcntl是最好的选择. FIOQSIZE : 返回文件或者目录大小, 用于设备返回 ENOTTY. FIONBIO : “File IOctl Non-Blocking I/O” 修改O_NONBLOCK flag in filp->f_flags, 第三个参数指明clear or set. 一般这个也用fcntl 来做. * fcntl的存在是因为历史原因, unix早期只有tty用ioctl, 所以-ENOTTY为啥是没有命令字的返回值 访问第三个参数 * copy from/to user依然可用 *<asm/uaccess.h> int access_ok(int type, const void *addr, unsigned long size); 第一个参数是VERIFY_READ or VERIFY_WRITE, 如果返回0( 访问失败) 要返回 -EFAULT access_ok 仅验证访问地址是"有道理的", 并且不在kernel space 1-8 bytes 的user space访问 put_user(datum, ptr) : 包含access ok 调用 __put_user(datum, ptr): * ptr 类型指明了传输的数据大小, 1-8 bytes * 成功返回0, 否则-EFAULT get_user(local, ptr) __get_user(local, ptr) * local, 一个局部变量 如果上面的宏size不合适会有很奇怪的编译错错误提示, 例如: “conversion to non-scalar type requested." 之类, 此时请用copy_from/to_user 附加权限检查 比 如,你可以读写磁盘, 但是不代表你可以格式化磁盘, 如果文件系统并没有对应权限检查, driver就要做. linux对传统的unix权限检查作了增强, 传统的unix 管理员拥有所有权利, 而linux可以按照能力, 吧权利下放到一些特定user或者程序. 新增了两个系统调用capget and capset可以做此权限设定. <linux/capability.h>中对于driver有关的: CAP_DAC_OVERRIDE The ability to override access restrictions (data access control, or DAC) on files and directories. CAP_NET_ADMIN The ability to perform network administration tasks, including those that affect network interfaces. CAP_SYS_MODULE The ability to load or remove kernel modules. CAP_SYS_RAWIO The ability to perform “raw” I/O operations. Examples include accessing device ports or communicating directly with USB devices. CAP_SYS_ADMIN A catch-all capability that provides access to many system administration operations. CAP_SYS_TTY_CONFIG The ability to perform tty configuration tasks. 内核的api: <linux/sched.h>): int capable(int capability); scrull提供个各种交换数据的手段, 一般用一种即可: 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 */ Device Control Without ioctl 比 如tty的escpe序列, 这样可以远程配置. 在比如AT command. 或者很多玩具设备, 没有数据可传递, 只有命令: 比如一个能用写数据的方式控制的camera. 也可以反其道而行, 将write read 省略, 只留下ioctl, 这样将复杂性转移到usr space, 让driver尽力的简单 Blocking IO sleeping rule: 1) 不要在atomic context sleep, 即持有锁的时候不要sleep: spin, seqlock, RCU lock, 或者禁止中断. 2) 持有sem虽然可以sleep,但是需要非常注意:你持有的sem, 不能最后block了唤醒你的进程. 3) 被唤醒后,一切都要重新来过: 你不知道睡眠了多久, 也不知道是否有其他进程要获取同样资源, 所有需要重新检查资源,看是否处于所需状态. 4) 确定有人可以唤醒你 wait queue: <linux/wait.h> DECLARE_WAIT_QUEUE_HEAD(name); or dynamicly as follows: wait_queue_head_t my_queue; init_waitqueue_head(&my_queue); wait event wait_event(queue, condition) wait_event_interruptible(queue, condition): 返回非0,需要返回-ERESTARTSYS(或者其他, 见前面章节) wait_event_timeout(queue, condition, timeout) : timepout后也返回0, 不管condition是否满足 wait_event_interruptible_timeout(queue, condition, timeout): timeout的单位是jiffers 这是一组宏, 所以queue是变量本身,而不是指针, condition是一个表达式, 进程被唤醒后检查condition是否为true. void wake_up(wait_queue_head_t *queue);:唤醒队列上所有进程. 对应wait_event void wake_up_interruptible(wait_queue_head_t *queue); : 对应wake_event_interruptible 如果是可中断的睡眠, 两个唤醒函数没有区别. wait_event_interruptible(wq, flag != 0); flag = 0; //注意如果多个进程被唤醒, 会产生竞争. 这个置零操作不能保证其他所有被唤醒进程保持睡眠状态.: 他们在wait_event_interruptible的时候可能都认为flag非零, 因为在多cpu的环境下(或者抢占), 可能来不及将flag置0, 导致都认为flag是非0. Blocking and Nonblocking Operations * O_NONBLOCK flag in filp->f_flags, <linux/fcntl.h> (include by<linux/fs.h>): “open-nonblock,” *O_NDELAY flag; this is an alternate name for O_NONBLOCK(for sys V code) 没有O_NONBLOCK时的read write 行为: • If a process calls read but no data is (yet) available, the process must block. The process is awakened as soon as some data arrives, and that data is returned to the caller, even if there is less than the amount requested in the count argument to the method. • If a process calls write and there is no space in the buffer, the process must block, and it must be on a different wait queue from the one used for reading. When some data has been written to the hardware device, and space becomes free in the output buffer, the process is awakened and the write call succeeds, although the data may be only partially written if there isn’t room in the buffer for the count bytes that were requested. 接 受buffer 可以防止丢失数据(如果没有人read), 而write 则不会丢失数据, 数据一直存在于user space. 发送buffer可以改善发送速度: 可以多接受数据, 从而wirter 不用loop loop... (每次也许还要返回到user space) 如果O_NONBLOCK置位, 则在read没有数据, wite没有buffer的情况下, 需要返回-EAGAIN. 处理non block 文件需要特别注意, 一定要检查errno, 否则返回0 和 EOF 重复了. open 也要注意O_nonblock, 因为open也会wait资源.如果open支持non block, 可以返回-EAGAIN, 然后开始初始化. 也有的设备open的时候对nonblock的处理语意有所不同,如tape, 指定nonblock,导致open返回成功(因为, 可能机器里没tape, 不是初始化就行的). 只有read, write, and open受non block的影响. *-ERESTARTSYS被fs处理: 返回-EINTR,或重新启动系统调用. * copy_to_user也会睡眠, 但是一般不用考虑, 这个调用不会使用driver的sem的. 高级sleep方法 2.6内核, 一般不用直接操作进程状态了, 但是需要的话,用接口函数: void set_current_state(int new_state); a.加入等待队列 b.set_current_state( interruptible or not) c.if (!condition) // 不重新检测条件的话可能错过wake up d. schedule( ); //但是这里还是有'间隙',(检查condition 到 schedule) 不会错过wak up? e. 恢复state为 running (因为进程可能根本不是被wakeup的) 注: 情况应该是这样的, 在设置了set_current_state这个间隙, 如果错过wakup情况是这样的: 如果没有c, 在b之前有wakup(设置为runalble, current->state=0, 调入run queue)被失去, 就会真的失去醒来的机会. 在b之前失去的wakup,c能检查到, 而b之后失去wakup就没有关系了, 因为在schdule中会检查进程状态, 如果被wakeup重新置为runable(stat==0), 将不会从runqueue删除, 参考schedule的代码: if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { if (unlikely(signal_pending_state(prev->state, prev))) prev->state = TASK_RUNNING; else deactivate_task(rq, prev, 1); switch_count = &prev->nvcsw; } 这里的逻辑关键是, 设置state的冲突问题. 手动睡眠步骤: *<linux/sched.h> *DEFINE_WAIT(my_wait);或者 wait_queue_t my_wait; init_wait(&my_wait); *void prepare_to_wait(wait_queue_head_t *queue,wait_queue_t *wait,int state);//入等待队列, 改进程状态 *if(need to sleep) schedule * void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait); * 检查条件是否满足, 不行再来 /* 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; 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; } Exclusive waits An exclusive wait acts very much like a normal sleep, with two important differences: • When a wait queue entry has the WQ_FLAG_EXCLUSIVE flag set, it is added to the end of the wait queue. Entries without that flag are, instead, added to the beginning. • When wake_up is called on a wait queue, it stops after waking the first process that has the WQ_FLAG_EXCLUSIVE flag set. 替换上述一个函数即可: void prepare_to_wait_exclusive(wait_queue_head_t *queue,wait_queue_t *wait, int state); The details of waking up The default wakeup function* sets the process into a runnable state and, possibly, performs a context switch to that process if it has a higher priority. Device drivers should never need to supply a different wake function; should yours prove to be the exception, see <linux/wait.h> for information on how to do it. wake_up(wait_queue_head_t *queue);//所有非exclusive 和一个exclusive wake_up_interruptible(wait_queue_head_t *queue); //同上, 跳过uninterruptible的 wake_up_nr(wait_queue_head_t *queue, int nr); // 指定唤醒的exclusive waiter的个数, 0代表所有 wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); wake_up_all(wait_queue_head_t *queue);//忽略exclusive, 全部唤醒 wake_up_interruptible_all(wait_queue_head_t *queue); wake_up_interruptible_sync(wait_queue_head_t *queue); 一 个被唤醒的进程可能抢占当前进程,在wake_up返回之前开始运行.如果调用wakup的进程在atomic context (it holds a spinlock, for example, or is an interrupt handler), 就没有这个问题. 如果需要明确要求不要被调度出去, 可以用这个函数. 一般来说wakup后还有很少工作要做,然后就可以sleep了, 可以使用这个函数. 驱动一般只用except wake_up_interruptible Ancient history: sleep_on void sleep_on(wait_queue_head_t *queue); void interruptible_sleep_on(wait_queue_head_t *queue); 绝对别用了, 没有保护,可能丢失wakup. 看一个nonblock的应用程序: int main(int argc, char **argv) { int delay = 1, n, m = 0; if (argc > 1) delay=atoi(argv[1]); fcntl(0, F_SETFL, fcntl(0,F_GETFL) | O_NONBLOCK); /* stdin */ fcntl(1, F_SETFL, fcntl(1,F_GETFL) | O_NONBLOCK); /* stdout */ while (1) { n = read(0, buffer, 4096); if (n >= 0) m = write(1, buffer, n); if ((n < 0 || m < 0) && (errno != EAGAIN)) break; sleep(delay); } perror(n < 0 ? "stdin" : "stdout"); exit(1); } poll and select poll/epoll/select是常见的nonblocking的文件处理技术. select是BSD unix引入的, poll是SystemV引入的, epoll 是linux2.5.45引入的.驱动需要实现poll这个接口来支持上述操作 . unsigned int (*poll) (struct file *filp, poll_table *wait); poll需要做两件事情, <linux/poll.h> 1) 调用poll_wait, 把自己的wait queue 和poll_table wait建立联系, 并加入自己的wait queue(自动分配新的, 已有就不再分配, 都处理好了), 这样调用poll的进程在某个fd有效后会得到通知 2) 返回一个位图,指示有什么操作可以立即非block的执行 返回的bit mask的说明: POLLIN :user有数据可以无block的read POLLRDNORM : 普通数据可以read,一个可以的设备返回 (POLLIN | POLLRDNORM). POLLRDBAND : out band 数据可读,只有 DECnet 用了下 POLLPRI :高优先级(out-of-band)数据可读, 这个bit指示select返回一个异常.带外数据就是作为异常情况处理的 POLLHUP :设备链接断开, socket 收到fin之类. (eof不是返回这个,ldd3的表述有些不严谨问题,文件系统么有实现poll的). 只是设备返回pollhup, select报告可读, 然后read返回0, 代表eof, 设备的eof.) POLLERR : 出错, 并且可read, 可write(总会返回出错). POLLOUT : 可以write POLLWRNORM : 和 POLLOUT, 一样, 可写设备应该返回: (POLLOUT | POLLWRNORM). POLLWRBAND: 和 POLLRDBAND类似, 代表带外数据发送可用, 也就 datagram 实现了这个东西. 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; } Interaction with read and write Reading data from the device • 如果有数据可读,read 会立即返回, 即便没有请求的那么多,即便新的数据马上到达.总是可以返回少于要求的数据.只要有1个byte即可返回,此时poll应该返回POLLIN|POLLRDNORM. • 如果没有数据, 默认read会block, 至少等到有1byte数据可读.如果O_NONBLOCK , read 立即返回, 返回值是-EAGAIN (有些老的System V 返回0!!!). 此时poll不应置可读位. •如果eof,(对设备来讲就是断开, fin了等) , read 应该返回0, poll 返回 POLLHUP. Writing to the device • 如果设备可写, write应该立即返回, 可以写入比要求少的数据,但是至少1bytes, poll 返回POLLOUT|POLLWRNORM. • 如果不可写, 默认是block, 至少等到1bytes 可写, 如果O_NONBLOCK , 立即返回-EAGAIN (older System V Unices returned 0). POLL不能置可写位. 如果再也不能写入任何数据, 返回-ENOSPC (“No space left on device”), 不管有没有O_NONBLOCK.. • 不要让write 等待数据传输,即便是O_NONBLOCK 没有设置. 如果要等待传输完成, 应该提供fsync 还是有些设备稍有例外, 如record-oriented devices (such as tape drives) 不能只写入部分数据. Flushing pending output int (*fsync) (struct file *file, struct dentry *dentry, int datasync); 上边提到过了 fsync 必须等到硬件确实完成了其工作才能返回, 这个调用不考虑O_NONBLOCK, datasync 用于区分fsync and fdatasync 这两个系统调用, 只有文件系统感兴趣这个参数, driver可以忽略. char driver一般不实现这个函数, 而block则 调用通用的实现:block_fsync. poll的实现简介 * driver 的poll实现可能收到NULL poll_table, 比如app调用poll的时候timeout是0(表示不等待) * epoll: 一次性建立poll_table队列, 然后就不用每次建立新的了 Asynchronous Notification user space: 1) fcntl, F_SETOWN 命令, 设置自己为owner, filp->f_owner保存这个进程id 2) F_SETFL fcntl命令设置FASYNC标记 这样新的数据到达时, 文件将向这个注册的进程(进程组,如果是负值)发送SIGIO信号. signal(SIGIO, &input_handler); /* dummy sample; sigaction( ) is better */ fcntl(STDIN_FILENO, F_SETOWN, getpid( )); oflags = fcntl(STDIN_FILENO, F_GETFL); fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC); 应用程序一般仅假设sokets和tty才有这个异步通知能力.但是同时监测多个文件,还是需要poll/select来查询哪个文件发生了变动. The Driver’s Point of View 1. When F_SETOWN is invoked, nothing happens, except that a value is assigned to filp->f_owner 2. app调用 F_SETFL 设置 FASYNC的时候, driver的fasync被调用. 3. 数据到达后,向注册的进程(进程组)发送SIGIO signal. 具体的, kernel给driver提供了统一的方法来管理 *<linux/fs.h> *struct fasync_struct *int fasync_helper(int fd, struct file *filp,int mode, struct fasync_struct **fa); //处理FASYNC的变化, 就最后一个参数driver提供, 其他的直接用fasync的就成. void kill_fasync(struct fasync_struct **fa, int sig, int band); //sig一般是SIGIO, band 一般是POLL_IN 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); } 当数据到达的时候:(对scull是在wite中) if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); //如果是wire可用通知,当然要换成POLL_OUT 最后在realease中调用如下函数,清除注册的进程: /* remove this filp from the asynchronously notified filp's */ scull_p_fasync(-1, filp, 0); /*一般在filp->f_flags 设置了 FASYNC才调用这个函数, 不过直接调用也没有关系*/ The llseek Implementation llseek 同时实现lseek and llseek系统调用. 系统默认的实现是修改filp->f_pos, 如果觉得不够可以自己实现一个. 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; } 如果不支持seek: *<linux/fs.h>. *在open 中调用int nonseekable_open(struct inode *inode; struct file *filp); *把你的llseek指针设置为:no_llseek Access Control on a Device File tty就很典型, 需要只有一个user, 但是改owenr的方式似乎不是那么好:open前需要root来修改owner, 挺怪. Single-Open Devices 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 */ } The release call, on the other hand, marks the device as no longer busy: static int scull_s_release(struct inode *inode, struct file *filp) { atomic_inc(&scull_s_available); /* release the device */ return 0; } Restricting Access to a Single User at a Time spin_lock(&scull_u_lock); if (scull_u_count && (scull_u_owner != current->uid) && /* allow user */ (scull_u_owner != current->euid) && /* allow whoever did su */ !capable(CAP_DAC_OVERRIDE)) { /* still allow root */ spin_unlock(&scull_u_lock); return -EBUSY; /* -EPERM would confuse the user */ } if (scull_u_count = = 0) scull_u_owner = current->uid; /* grab it */ scull_u_count++; spin_unlock(&scull_u_lock); static int scull_u_release(struct inode *inode, struct file *filp) { spin_lock(&scull_u_lock); scull_u_count--; /* nothing else */ spin_unlock(&scull_u_lock); return 0; } Blocking open as an Alternative to EBUSY 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; /* tell the fs layer to handle it */ spin_lock(&scull_w_lock); } if (scull_w_count = = 0) scull_w_owner = current->uid; /* grab it */ scull_w_count++; spin_unlock(&scull_w_lock); static int scull_w_release(struct inode *inode, struct file *filp) { int temp; spin_lock(&scull_w_lock); scull_w_count--; temp = scull_w_count; spin_unlock(&scull_w_lock); if (temp = = 0) wake_up_interruptible_sync(&scull_w_wait); /* awake other uid's , 非常符合sync的要求*/ return 0; } 这个实现对于cp/tar之类不太好, 应为他们不能加上O_nonblock参数, 等待的话很损害交互性, user更愿意看到device busy之类的消息. 这时可以实现多个设备文件,采用不动的访问策略. Cloning the Device on open 比如tty. 这里 /dev/scullpriv就是这样一个设备. 可以很容易更改判断的key来得到不通策略. 这里用tyy. 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; } |
相关阅读 更多 +