linux字符设备命令,Linux字符设备驱动进阶

在之前讨论的字符设备驱动,只实现了open、release、read、write等基本操作,现在我们讨论一下高级字符设备驱动的一些接口,如:ioctl、poll、非阻塞IO等等。

1.ioctl

ioctl提供对设备的控制能力,一般ioctl传递的数据量非常小。

用户空间的ioctl:

int ioctl(int fd,unsigned long cmd,...)

内核(驱动)空间的ioctl:

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

1.1 ioctl定义命令:

定义ioctl 命令的正确方法是使用4个位段, 这个列表中介绍的符号定义在中:

1) Type

幻数(类型): 表明哪个设备的命令,在参考了ioctlnumber.txt之后选出,8 位宽。

2) Number

序号,表明设备命令中的第几个,8 位宽

3) Direction

数据传送的方向,可能的值是_IOC_NONE(没有数据传输),_IOC_READ, _IOC_WRITE。数据传送是从应用程序的观点来看待的,_IOC_READ 意思是从设备读。

4) Size

用户数据的大小。(13/14位宽,视处理器而定)

内核提供了下列宏来帮助定义命令:

1) _IO(type,nr)

没有参数的命令

2) _IOR(type,nr,datatype)

从驱动中读数据

3) _IOW(type,nr,datatype)

写数据到驱动

4) _IOWR(type,nr,datatype)

双向传送,type 和number 成员作为参数被传递。

定义命令(范例)

#define MEM_IOC_MAGIC ‘m’ //定义幻数

#define MEM_IOCSET

_IOW(MEM_IOC_MAGIC, 0, int)

#define MEM_IOCGQSET

_IOR(MEM_IOC_MAGIC, 1, int)

1.2 ioctl函数实现

定义好了命令,下一步就是要实现Ioctl函数了,Ioctl函数的实现包括如下3个技术环节:

1) 返回值

ioctl函数的实现通常是根据命令执行的一个switch语句。但是,当命令号不能匹配任何一个设备所支持的命令时,通常返回-EINVAL(“非法参数”)。

2) 参数使用

如果是一个整数,可以直接使用。如果是指针,我们必须确保这个用户地址是有效的,因此使用前需进行正确的检查。一般使用下面四个函数或宏:

不需要检测(因为内部已经调用access_ok做了检测):

1) copy_from_user : 大量数据

2) copy_to_user :大量数据

3) get_user : 少量数据

4) put_user :少量数据

需要检测:

1) __get_user

2) __put_user

检测函数:int access_ok(int type, const void *addr, unsigned long size)

3) 命令实现

很简单,只需要一组switch...case,实现各个CMD的操作。

2.阻塞IO与非阻塞IO

当读设备数据,数据还没准备好时(读缓冲区为空),或者向设备写入数据,写入缓冲区已满状态时,如果是阻塞IO,则需要进入休眠,如果是非阻塞IO,则立即返回,并设置相应的errno(-EAGAIN)。

2.1阻塞IO的休眠

方法1:等待事件的休眠:

Linux内核中最简单的休眠方式是称为wait_event的宏(以及它的几个变种),它使queue作为等待队列头的等待队列中所有属于该等待队列对应的进程休眠;在实现休眠的同时,它也检查进程等待的条件,当条件condition满足且queue作为等待队列头的等待队列被唤醒时,函数返回。wait_event的形式如下所示:

wait_event(queue, condition )

wait_event_interruptible(queue, condition)

wait_event_timeout(queue, condition, timeout)

wait_event_interruptible_timeout(queue, condition, timeout)

wait_event()和wait_event_interruptible()区别在于后者可以被信号打断,而前者不会。 wait_event_interruptible_timeout()可以设置一个timeout。

方法2:直接操作进程状态(不推荐使用这种方式)

还可以通过直接操作进程状态,用显示方式使进程休眠,如:

__set_current_state(TASK_INTERRUPTIBLE);

schedule(); //CPU调度其他进程执行

如果有唤醒该进程的条件发生,如修改进程状态,进程会继续schedule()之后的语句。

方法3:手工休眠(等待队列)

这是早期的Linux内核版本中的休眠方法,但是在内核中也在大量使用。

初始化一个等待队列:

wait_queue_t my_wait;

init_wait(&my_wait);

将我们的等待队列入口添加到队列中,并设置进程状态,可通过下面的函数完成:

void prepare_to_wait(wait_queue_head_t *queue, wait_quere_t *wait, int state);

在调用prepare_to_wait后,进程可调用schedule,一旦schedule返回,就到了清理阶段了,可以用下列函数完成:

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);

此后,代码可测试其状态,判断是否需要重新等待。

注意:永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或者 RCU 锁时不能睡眠;关闭中断也不能睡眠。

2.2唤醒进程

其他的某个执行线程(可能是另一个进程或者中断处理例程,注意:中断处理例程不属于进程上下文)必须为我们执行唤醒,因为我们的进程正在休眠中。而用来唤醒休眠进程的基本函数是wake_up(),它也有多种形式:

void wake_up(wait_queue_head_t *queue)

void wake_up_interruptible(wai_queue_head_t *queue)

上述操作会唤醒queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。 注意:

wake_up()应该与wait_event()或者wait_event_timeout()成对使用,wake_up_interruptible()则应该与wait_event_interruptibl() 或wait_event_interruptible_timeout() 成对使用。wake_up()可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_UNINTERRUPTIBLE的进程。

3.IO多路复用(poll)

我们知道在Linux系统调用中有三个IO多路复用函数,select、poll、epoll,但是在内核中,都是通过file_operations结构体的poll函数指针实现的。

unsigned int (*poll) (struct file *filp, struct poll_table *wait);

//第一个参数为file结构体指针,第二个参数为轮询表指针。

这个函数应该进行以下两项工作:

(1)对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应等待队列添加到poll_table中;

(2)返回表示是否能对设备进行无阻塞可读或可写访问的掩码;

位掩码:POLLRDNORM, POLLIN,POLLOUT,POLLWRNORM

设备可读,通常返回:(POLLIN | POLLRDNORM)

设备可写,通常返回:(POLLOUT | POLLWRNORM)

4.异步通知

异步通知:一旦设备就绪,则主动通知应用程序,应用程序根本就不需要查询设备状态。(类似于中断)信号是异步的,一个进程不必通过任何操作来等待信号的到达。

在linux中,异步通知是使用信号来实现的,而在linux,大概有30种信号,比如大家熟悉的ctrl+c的sigint信号,进程能够忽略或者捕获除过SIGSTOP和SIGKILL的全部信号,当信号背捕获以后,有相应的函数来处理它

4.1应用程序启用文件的异步通知机制

必须执行两个步骤:

1)指定一个进程作为文件的"属主",进程可以使用fcntl执行F_SETOWN命令(此时进程id号被保存在filp->f_owner中),目的是为了让内核知道应该通知哪个进程。

2)用户通过fcntl的F_SETFL命令设置FASYNC标志。

执行完上述两个步骤后,输入文件可以在数据到达时请求发送一个ISGIO信号(驱动程序发送),该信号发送到存放在filp->f_owner中的进程。

如下示例代码启用stdin文件的异步通知机制:

signal(SIGIO,&input_handler);

fcntl(STDIN_FILENO,F_SETOWN,getpid());

oflags=fcntl(STDIN_FILENO,F_GETFL);

fcntl(STDIN_FILENO,F_SETFL,oflags|FASYNC);

应用程序中还有两点注意:应用程序不是所有设备都支持异步通知,通常应用程序假设只有套接字和终端才有异步通知能力;如果有多个文件可以异步通知输入的进程,应用程序需要借助poll或者select来确定输入的来源(利用FD_ISSET来判断)。

4.2驱动程序实现异步信号

内核已经提供了很方便的函数给我们使用,为了实现异步信号,驱动程序需要做三件事情:

1)实现fasync方法:该方法也只需要做一步,调用内核提供的fasync_helper函数,如下是scullp设备提供的实现代码:

staticintscull_p_fasync(intfd,struct file*filp,intmode)

{

struct scull_pipe*dev=filp->private_data;

return fasync_helper(fd,filp,mode,&dev->async_queue);

}

2)当数据到达时,需要实现异步通知,这时需要调用kill_fasync,当数据可读时,此时需要通知应用程序数据可读,如下是scullp设备提供的代码:

if(dev->async_queue)

kill_fasync(&dev->async_queue,SIGIO,POLL_IN);

如果是为写入提供异步信号,kill_fasync必为模式调用POLL_OUT。

3)在文件关闭时必须调用fasync方法,以便从活动的异步读取进程列表中删除该文件,所有在close方法中有如下调用:

sucll_p_fasync(-1,filp,0);

5.定位设备ilseek

llseek是修改文件中的当前读写位置的系统调用。内核中的缺省的实现进行移位通过修改 filp->f_pos, 这是文件中的当前读写位置。对于 lseek 系统调用要正确工作,读和写方法必须通过更新它们收到的偏移量来配合。

如果设备是不允许移位的,你不能只制止声明 llseek 操作,因为缺省的方法允许移位。应当在你的 open 方法中,通过调用 nonseekable_open 通知内核你的设备不支持 llseek :

int nonseekable_open(struct inode *inode; struct file *filp);

完整起见, 你也应该在你的 file_operations 结构中设置 llseek 方法到一个特殊的帮助函数 no_llseek(定义在 )。

6.设备文件的访问控制

1)独享设备

最生硬的访问控制方式是只允许一个设备一次被一个进程打开(独享),这是一个设备驱动最简单的访问控制。

2)单用户访问

open 调用在第一次打开记住了设备拥有者,此用户可多次打开设备,并协调多个进程对设备并发操作。同时,没有其他用户可打开它,避免了外部干扰。

3)阻塞型单用户访问

实现一个阻塞open,在打开设备时等待其他进程释放,待他人释放后,open才返回,不过这样交互性不太好。

4)在 open 时复制设备

访问控制的另一个技术是根据打开条件创建不同的设备私有副本。在进程打开设备时,同时创建设备的不同私有副本。适用于设备没有绑定到某个硬件时才能实现,也就是适用于/dev/tty等虚拟设备。 /dev/tty 的内部使用类似的技术来给它的进程一个不同的 /dev 入口点所呈现的“景象”。这类访问控制较少见,但这个实现可说明内核代码可以轻松改变应用程序的运行环境,类似windows中的虚拟机概念。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值