[读书笔记]高级字符驱动程序(第六章)

综述

在本章中,我们要掌握以下知识点
ioctl接口的使用
如何使进程休眠并且唤醒
如何实现非阻塞I/O
在设备可写入或者读取时如何通知用户空间

1. ioctl

ioctl函数解析

除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制,通过ioctl可以实现!
在用户空间,ioctl系统调用原型

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

第三个参数...代表可变参数

在内核空间的ioctl

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

inode和filp两个指针的值对应于应用程序传递的文件描述符fd,和open方法是一样的
参数cmd又用户空间不经修改地传递给驱动程序
可选参数arg:无论是用户程序使用的是指针还是整数值,都以unsigned long的形式传递给驱动程序

选择ioctl命令

一般来说我们本能地会从0或者1开始编号,但是这样无法保证每个ioctl命令是唯一的,因此Linux给我们提供了自己的方案。

定义命令号的方法使用了4个字段,包含在头文件
type
幻数。选择一个号码,并在整个驱动程序中使用这个号码,该字段有8位宽(_IOC_TYPEBITS)
number
序数(顺序编号)。也是8位宽(_IOC_NRBITS).
derection
数据的传输方向,类型包括
_IOC_NONE(没有数据传输)
_IOC_READ(读取数据)
_IOC_WRITE(写入数据)
_IOC_READ | _IOC_WRITE(双向传输数据-可读可写)
注意:这里的数据传输是用应用程序的角度来看的,因此
_IOC_READ代表从设备中读取数据,所以驱动程序必须向用户空间写入数据。
size
用户数据的大小,该字段宽度和体系结构有关,通常是13-14位,具体可以查找宏_IOC_SIZEBITS来确定,系统并不强制使用这个字段!

构造命令的宏

包含头文件定义的已结构造命令的宏
_IO(type,nr):构造无参数的命令编号
_IOR(type,nr,datatype):构造从驱动程序中读取数据的命令编号
_IOW(type,nr,datatype):构造往内核空间写入数据的命令编号
_IOWR(type,nr,datatype):构造双向数据传输的命令编号

参数:type和nr(number)通过参数传入,而size字段通过对datatype参数取sizeof()获得

解开位字段的宏

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

构造IOCTL命令示例

/*使用k作为幻数*/
#define MY_IOC_MAGIC 'k'
/**在你自己的代码中,请使用8位数字*/
/*
*S 表示设置(Set)-通过指针设置(Set) 
*T 表示通知 (Tell)-直接使用参数值 通知 (Tell)
*G 表示获取(Get)-通过设置指针来应答
*Q 表示查询(Query)-通过返回值应答
*X 表示交换(eXchange)-原子的交换G和S
*H 表示切换(sHift)-原子的交换T和Q
*/
//自定义IOCTL命令 使用以上的宏
#define MY_IOC_UANTUM    _IOW(MY_IOC_MAGIC,1,int);
#define MY_IOC_SQ_SET    _IOW(MY_IOC_MAGIC,2,int);
#define MY_IOC_TQ_SET    _IO(MY_IOC_MAGIC,3);
#define MY_IOC_SQ_GET    _IOR(MY_IOC_MAGIC,4,int);

另一种定义命令的方式就是显示地声明一组数字,但也会带来很多问题,
头文件
就是这种旧风格的使用方式,使用了16位标量数值来定义ioctl命令,这是因为那时候只有这种方式!!!

返回值

ioctl的实现通常就是基于命令号的switch语句,若是未能匹配任何ioctl命令,默认返回-ENVAL(Invalid argument,非法参数)。

预定义命令

有些命令内核已经定义好了,因此我们定义的命令不能和内核的冲突,否则就接收不到我们自己的命令。

预定义命令分为三组:
可用于任何文件(普通、设备、FIFO和套接字)的命令
只用于普通文件的命令
特定文件系统类型的命令
设备驱动开发人员只对第一组感兴趣,他们的幻数都是"T"
ext2_ioctl,实现了只追加标志(append-only)和不可变标志(immutable)
以下的ioctl命令对任何文件都是预定义的:
FIOCLEX
设置执行时关闭标志(File IOctl CLose on Exec)。设置了这个标志之后,当调用进程执行一个新程序时,文件描述符将被关闭
FIONCLEX
清除执行时关闭标志(File IOctl Not CLose on EXec)。该命令将恢复通常的文件行为,并撤销上述FIOCLEX命令所做的工作。
FIOASYNC
设置或复位文件异步通知,注意知道Linux 2.2.4 内核都不正确使用这个命令修改O_SYNC标志。因为这两个动作可以通过fcntl完成,所以实际上没人使用这个命令。
FIOQSIZE
该命令返回文件或者目录的大小。不过当用于设备文件时,会导致ENOTTY错误的返回
FIONBIO
"File IOctl Non-Blocking I/O",文件ioctl非阻塞型I/O。改调用秀阿贵filp->f_flags中的O_NONBLOCK标志。传递系统调用的第三个参数指明了是设置还是清除该标准。修改该标志的常用方法是由fcntl系统调用使用F_SETFL命令来完成。

使用ioctl参数

参数中指针合法检测
注意,ioctl附加参数,如果是整数可以直接使用,如果是指针,当该指针指向用户空间时,要检查该地址是否合法。

int access_ok(int type,const void *addr,unsigned long size);
用于验证地址是否合法
type:VERIFY_READ或者VERIFY_WRITE(包含VERIFY_READ),取决于执行动作是读取还是写入用户空间。
addr:用户空间地址
size:字节数
返回值:1-成功,0-失败

注意2点:
1.access_ok没有完成验证内存的全部工作,只是检查了引用的内存是否位于进程有对应访问权限的区域内。
2.大多数驱动程序都没必要调用access_ok,内存管理程序会处理它。
代码示例
方向是一个位掩码,而VERUFY_WRITE用于R/W输出
类型是真的用户空间而言的,而access_ok是面向内核的
因此读取和输出恰好是相反的

if(_IOC_DIR(cmd) & _IOC_READ)
    err = !access_ok(VERIFY_WRITE,(void __user *)arg,_IOC_SIZE(cmd));

用户空间和内核空间交换数据的方式
1.copy_from_user(),copy_to_user();
2.
put_user(datum,ptr);
__put_user(datum,ptr);
这些宏把datum写到用户空间,速度快。传递单个数据时使用该函数而不是copy_to_user();
__put_user(datum,ptr)比put_user(datum,ptr)做的检查少一些,不调用access_ok,因此使用__put_user之前应该调用access_ok

get_user(local,ptr)
__get_user(local,ptr)
这些宏用于从用户空间接收数据,保存到locak中,除了传输方向不同,其他与put宏一样。

权能
#include
定义了各种CAP_*符号,用于描述用户空间进程拥有的权能操作
int cap(int capability)
如果进程具有指定的权能,返回非零值。

2.如何使进程休眠并且唤醒

休眠含义
简单的说,休眠是一种进程的特殊状态(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)

休眠是为在一个当前进程等待暂时无法获得的资源或者一个event的到来时(原因),避免当前进程浪费CPU时间(目的),将自己放入进程等待队列中,同时让出CPU给别的进程(工作)。休眠就是为了更好地利用CPU。
一旦资源可用或event到来,将由内核代码(可能是其他进程通过系统调用)唤醒某个等待队列上的部分或全部进程。从这点来说,休眠也是一种进程间的同步机制。
休眠的规则
1.永远不要在原子上下文中进入休眠
2.唤醒时必须检测等待的条件真正为真
等待队列
除非我们知道其他人会在某个地方唤醒休眠的进程,否则进程就不能进入休眠,因此,Linux维护了一个称为等待队列的数据结构(链表)。
在Linux中,一个等待队列通过"等待队列头(wati queue head)"来管理。
定义并初始化
#include
静态方法:
DEAKARE_WAIT_QUEUE_HEAD(name);
动态方法:
wait_queue_heat_t my_queue;
init_waitqueue_head(&my_queue);
休眠的方式
Linux中实现休眠最简单的方式:wait_evet()以及他的变种
wait_event(queue,condition):非中断休眠
wait_event_interruptible(queue,condition):可以被信号中断休眠,返回值非零时表示休眠被某个信号中断了。
wai_event_timeout(queue,condition,timeout)
不可中断休眠,给定时间(以jiffy表示)到期时,返回0,无论condition是否为真
wai_event_interruptible_timeout(queue,condition,timeout)
可中断休眠,给定时间(以jiffy表示)到期时,返回0,无论condition是否为真

参数:queue是指针队列的头,通过值传递,而不是指针。
参数:condition是任意一个布尔表达式,该条件为真之前,进程都会休眠。

唤醒的方式
void wake_up(wait_queur_head_t *queue):
唤醒所有进程
void wake_up_interruptible(wait_queue_head_t *queue);
唤醒可中断进程
阻塞和非阻塞
有时候调用进程会通知我们它不想阻塞,无论I/O是否可以继续。显示的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定,包含在中,该头文件又包含在中,注意O_NDELAY和O_NONBLOCK是一样的!

3.如何实现非阻塞I/O

通常在驱动程序内部,阻塞在read调用的进程在数据到达时被唤醒;通常硬件会发出一个中断来通知这个数据到来的事件,然后作为中断处理的一部分,驱动程序会唤醒等待进程。不过我们要编写的驱动不基于硬件,就没有中断,我们选择使用另一个进程来产生数据并且唤醒读取进程。类似的读取进程用来唤醒等待缓冲区空间的写入进程!

struct scull_pipe {
  wait_queue_head_t inq,outq;//读取和写入队列
  char *buffer,*end;//缓冲区的起始和结尾
  int buffersize;//用于指针计算
  char *rp,*wp;//读取和写入的位置
  int nreaders,nwriters;//读取和写入打开的数量
  struct fasync_struct *async_queue;//异步读取者
  struct semaphore sem;//互斥信号量
  struct cdev cdev;//字符设备结构
}

read时序负责管理阻塞型和非阻塞型输入

static ssize_t scull_p_read(struct file *filp,char __user *buf,size_t count,
                    loff_t *f_pos)
{
  struct scull_pipe *dev = filep->private_data;
  if(down_interruptible(&dev->sem))
    return -ERESTARTSYS;

  while(dev->rp = = dev->wp){//无数据可读
    up(&dev->sem);//释放锁
    //检查用户请求的是否是非阻塞I/O,如果是,直接返回,否则进入休眠
    if(filp->f_flags & O_NONBLOCK)
      return -EAGAIN;
    if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp)))
      return -ERESTARTSYS;//信号 通知fs层做相应处理
    //否则循环 但首先获取锁
    if(dowm_interruptible(&dev->sem))
      return -ERESTARTSYS;
  }
    //数据已就绪 
    if(dev->wp > dev->rp)
      count = min (count,(size_t)(dev->wp - dev->rp));
    else
      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;
    up(&dev->sem);
    //唤醒所有写入者并返回
    wake_up_interruptible(&dev->outq);
    return count;    
}

高级休眠
休眠步骤一:分配并且初始化一个wait_queue_t结构,然后将其加入等待队列中。
休眠步骤二:设置进程状态,将其标志为休眠。(Linux 2.6中,通常不需要驱动程序代码来直接操作进程状态)

TASK_RUNNING:进程可运行
TASK_INTERRUPTIBLE:可中断休眠状态
TASK_UNINTERRUPTIBLE:不可中断休眠状态

设置进程状态
void set_current_state(int new_state);
或者
current->state = TASK_INTERRUPTIBLE;

休眠步骤三:必须检查休眠等待的条件,如果不作这个检查,可能会导致竞态。

if(!condition)
  schedule();//该函数将调用调度器,让出CPU

手工休眠
该方式不推荐使用,仅仅做了解即可
独占等待
了解即可
唤醒更多细节
wake_up(wait_queue_head_t *q)
唤醒q队列上的所有非独占等待的进程,以及单个独占等待者(如果存在)
wake_up_interruptible(wait_queue_head_t *q)
和以上一样的工作,只是它会跳过不可中断休眠的那些进程。
wake_up_nr(wait_queue_head_t *q)
wake_up_interruptible_nr(wait_queue_head_t *q)
只会唤醒nr个独占等待进程,注意nr=0时表示唤醒所有独占等待进程
wake_up_all(wait_queue_head_t *q)
wake_up_interruptible_all(wait_queue_head_t *q)
上述形式函数,不管是否执行独占等待,均唤醒它们
wake_up_interruptible_sync(wait_queue_head_t *q)

你可能感兴趣的:([读书笔记]高级字符驱动程序(第六章))