欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-09 18:14:22
...

综述

在本章中,我们要掌握以下知识点
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个字段,包含在头文件<linux/ioctl.h>
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来确定,系统并不强制使用这个字段!

构造命令的宏

<linux/ioctl.h>包含<asm/ioctl.h>头文件定义的已结构造命令的宏
_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);

另一种定义命令的方式就是显示地声明一组数字,但也会带来很多问题,
头文件<linux/kd.h>
就是这种旧风格的使用方式,使用了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附加参数,如果是整数可以直接使用,如果是指针,当该指针指向用户空间时,要检查该地址是否合法。
<asm/uaccess.h>
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.<asm/uaccess.h>
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 <linux/capability.h>
定义了各种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 <linux/wait.h>
静态方法:
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标志决定,包含在<linux/fcntl.h>中,该头文件又包含在<linux/fs.h>中,注意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中,通常不需要驱动程序代码来直接操作进程状态)
<linux/sched.h>
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)