一执着书生,只求乱世太平
2013年(15)
分类: 嵌入式
2013-05-13 23:10:56
从此处开始,如不作特别说明,使用的代码都是基于2.6.38版本内核。
在设计简单字符设备驱动程序时跳过的一个问题:当设备无法立刻满足用户读写请求时该如何处理?比如应用程序调用read而无数据可读或数据不足,调用write而设备暂时无法接受数据或没有足够的空间写入数据,应用程序不会关心这种问题。这都需要通过驱动程序缺省的实现阻塞,使进程睡眠,直到请求可以满足。
阻塞是文件读写默认(缺省)方式。应用程序员在打开文件时设置O_NONBLOCK(#include
在Linux设备驱动程序中使用等待队列实现阻塞,等待队列中的进程都会进入睡眠状态。等待队列操作包括3步:定义及初始化、阻塞(进程添加到等待队列)、唤醒。定义及初始化等待队列
wait_queue_head_t mycdev_inq, mycdev_outq; /* 定义 */
init_waitqueue_head(&mycdev_inq); /* 初始化 */
init_waitqueue_head(&mycdev_outq);
/* 定义并初始化的令一种方法
* DECLARE_WAIT_QUEUE_HEAD(mycdev_inq);
*/
等待队列3种阻塞方式
// 一下3种阻塞方式都满足当condition=true立即返回
/* 阻塞时让进程进入TASK_UNINTERRUPTIBLE模式睡眠,并挂在mycdev_inq参数指定队列上 */
wait_event(mycdev_inq, condition);
/* 阻塞时让进程进入TASK_INTERRUPTIBLE模式睡眠,并挂在mycdev_inq参数指定队列上 */
wait_event_interruptible(mycdev_inq, condition);
/* 阻塞时让进程进入TASK_KILLABLE模式睡眠,并挂在mycdev_inq参数指定队列上 */
int wait_event_killable(mycdev_inq, condition);
进程唤醒方法
/* 从q中唤醒状态为TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE和TASK_KILLABLE的所有进程*/
wake_up(wait_queue_t *q);
/* 从q中唤醒状态为TASK_INTERRUPTIBLE的所有进程*/
wake_up_interruptible(wait_queue_t *q);
static const struct file_operations mycdev_fops = {
.unlocked_ioctl = mycdev_ioctl, /* IO控制*/
.poll = mycdev_poll,
.mmap = mycdev_mmap, /* 映射*/
};
每个操作的内核接口为
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
long (*unlocked_ioctl) (struct file *flip, unsigned int cmd, unsigned long arg);
flip:打开的设备文件
cmd:执行的命令
arg:用户空间传进来的参数,定义为ulong类型,也可以将指针传入
unlocked_ioctl方法实现步骤:
(1)定义命令cmd,需要包含#include
unlocked_ioctl的命令分为4个位段:类型(幻数,8bits)、序号(8bits)、传送方向、参数大小(13~14bits)。Documents/ioctl/ioctl-number.txt中罗列了对系统已使用的幻数,幻数指定了设备类型。序号表明设备命令中的第几号命令(一般从0开始,但不一定)。传送方向可以为_IOC_NONE(无数据传送)、_IOC_READ(从设备读)、_IOC_WRITE(写入设备),注意此处的传送方向都是指从应用程序的角度看的。参数大小指用户传送数据大小。
命令的构造通过内核提供的宏定义来定义:
_IO(type, nr) // 无参数命令
_IOR(type, nr, datatype) // 从驱动中读数据
_IOW(type, nr, datatype) // 写数据到驱动
_IOWR(type, nr, datatype) // 双向传送
datatype取值可以为一般的数据类型(int,long等),也可以是预定义的typedef类型(struct,enum等)。如下为mycdev模块中定义的命令
#define MYCDEV_IOC_MAGIC 'k'
#define MYCDEV_IOC_PRINT _IO(MYCDEV_IOC_MAGIC, 0)
#define MYCDEV_IOC_GETDATA _IOR(MYCDEV_IOC_MAGIC, 1, int)
#define MYCDEV_IOC_SETDATA _IOW(MYCDEV_IOC_MAGIC, 2, int)
#define MYCDEV_IOC_MAXNR (3)
(2)使用switch…case实现命令
当传入的命令为无效命令(不匹配任何已有的命令)时,unlocked_ioctl返回-EINVAL(“非法参数”)。
如果传入的arg只是整数,则直接可用;若arg传入的为指针,使用前必需对其进行检查,保证指针指向的用户地址有效,检查的方法如下
int access_ok(int type, const void *addr, unsigned long size);
type:取VERIFY_READ或VERIFY_WRITE
addr:指要操作的用户内存地址,为unlocked_ioctl传入的arg
size:操作数据的长度
成功返回1,否则返回0,返回0则unlocked_ioctl返回-EFAULT.
检查参数有效性之后就是使用switch…case实现命令了,具体实现参考本节中的代码。
在需要从用户空间获取小量数据时,可以使用__put_user和__get_user分别代替copy_to_user和copy_from_user,此时必须使用access_ok检查用户地址有效性。使用put_user和get_user可避免使用access_ok。
在分析file_operations中的mmap之前,先了解:系统调用的mmap负责将文件内容映射到进程的虚拟地址空间,然后可以直接对映射的虚拟内存空间执行读写操作而不必使用诸如read和write等去访问文件。mmap及munmap的系统调用如下
void *mmap(void *start, int length, int prot, int flags, int fd, int offset);
start:映射内存的起始地址,通常设为NULL由系统指定
length:需要映射到内存的文件长度
prot:映射区保护方式,有PROT_EXEC,PROT_READ,PROT_WRITE
flags:映射区特性,有MAP_SHARED(修改会写回文件,多个进程能映射同一地址实现共享),MAP_PRIVATE(修改不写回文件,写入操作会产生映射区的一个复制)
fd:文件描述符
offset:开始映射的文件偏移量
返回值:虚存区起始地址
int munmap(void *start, size_t length);
mmap的一个系统调用实例,
#include
#include
#include
#include
#include
#include
#include
#include
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define msg "abc"
int main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
struct stat statbuf;
if (argc != 3)
{
printf("usage: %s
return 0;
}
if ((fdin = open(argv[1], O_RDONLY)) < 0)
{
printf("err: can't open %s for reading/n", argv[1]);
return 0;
}
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
{
printf("err: can't create %s for writing/n", argv[2]);
return 0;
}
if (fstat(fdin, &statbuf) < 0)
{
printf("fstat error/n");
return 0;
}
/* set size of output file */
if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1)
{
printf("lseek error/n");
return 0;
}
if (write(fdout, "", 1) != 1)
{
printf("write error/n");
return 0;
}
if ((src = mmap(0, statbuf.st_size, PROT_READ , MAP_SHARED, fdin, 0)) == MAP_FAILED)
{
printf("mmap error for input/n");
return 0;
}
if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE , MAP_SHARED , fdout, 0)) == MAP_FAILED)
{
printf("mmap error for output/n");
return 0;
}
memcpy(dst, src, statbuf.st_size);
exit(0);
}
将设备映射指把用户空间的一段地址关联到设备内存上,当程序读写用户空间地址时实际上是在访问设备。file_operations中的mmap功能:建立虚拟地址到物理地址的页表。
int (*mmap) (struct file *, struct vm_area_struct *);
struct vm_area_struct是虚存区结构,是虚拟地址的数据结构描述.
在mmap中调用remap_pfn_range可一次建立所有的页表,完整实例参考“高级字符设备驱动实例”一节。
#include
/**
* remap_pfn_range - remap kernel memory to userspace
* @vma: user vma to map to
* @addr: target user address to start at
* @pfn: physical address of kernel memory
* @size: size of map area
* @prot: page protection flags for this mapping
*
* Note: this is only safe if the mm semaphore is held when called.
*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
本实例在“简单字符设备驱动”一节的基础上改进,添加了unlocked_ioctl的示例实现、read的阻塞操作和mmap的实现,将静态创建设备文件改为动态创建设备文件,实验中将MYCDEV_MAJOR设为0动态分配主设备号。
/*
* ===================================================================================
* Filename: mycdev.c
*
* Description:
*
* Version: 1.0 (02/26/2013)
* Created: xhzuoxin(xiahouzuoxin@163.com)
* Compiler: gcc
* ===================================================================================
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "mycdev.h"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xhzuoxin");
/*
* if 0 : dinamic alloc device number
* else : static device number
*/
#define MYCDEV_MAJOR (0)
struct mycdev {
struct cdev cdev;
unsigned char mem[MYCDEV_SIZE]; /* 使用内存区模拟字符设备 */
wait_queue_head_t inq, outq; /* 阻塞用等待队列 */
};
/* global variables */
static dev_t mycdev_major = MYCDEV_MAJOR; /* 主设备号 */
dev_t devno = 0; /* 设备号 */
struct mycdev *devp; /* 设备结构 */
struct class *myclass; /* 动态创建设备文件需要的class */
bool read_able = false; /* 是否有数据可读标志,初始化为flase */
static int mycdev_open(struct inode *inode, struct file *fp)
{
fp->private_data = devp; /* 通过private_data避免全局变量的使用 */
return 0;
}
static int mycdev_release(struct inode *inode, struct file *fp)
{
return 0;
}
static ssize_t mycdev_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{
unsigned long p = *pos;
unsigned int count = size;
struct mycdev *tmp_dev = fp->private_data;
/* 阻塞处理 */
while (!read_able) { /* 使用while,唤醒进程时也应该检测该标志 */
if (fp->f_flags & O_NONBLOCK) {
return -EAGAIN;
}
wait_event_interruptible(tmp_dev->inq, read_able); /* 等待处理 */
}
/* 读过程 */
if (p >= MYCDEV_SIZE) {
read_able = false;
return -1;
}
if (count > MYCDEV_SIZE - p) {
count = MYCDEV_SIZE - p;
read_able = false;
}
if (copy_to_user(buf, (void *)(tmp_dev->mem + p), count) != 0) {
printk("read error!\n");
return -1;
} else {
*pos += count; /* 修改偏移量 */
printk(KERN_INFO"read %d bytes from %ld\n", count, p);
}
return count;
}
static ssize_t mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
unsigned long p = *pos;
unsigned int count = size;
struct mycdev *tmp_dev = fp->private_data;
if (p > MYCDEV_SIZE) {
return -1;
}
if (p > MYCDEV_SIZE - count) {
count = MYCDEV_SIZE - p;
}
if (copy_from_user((void *)(tmp_dev->mem + p), buf, count) != 0) {
return -1;
} else {
*pos += count;
printk(KERN_INFO"write %d bytes from %ld\n", count, p);
}
read_able = true;
/* 唤醒阻塞进程 */
wake_up_interruptible(&(tmp_dev->inq));
return count;
}
static loff_t mycdev_llseek(struct file *fp, loff_t off, int whence)
{
// struct mycdev *dev = fp->private_data;
loff_t new_pos = 0;
switch(whence) {
case SEEK_SET:
new_pos = off;
break;
case SEEK_CUR:
new_pos = fp->f_pos + off;
break;
case SEEK_END:
new_pos = MYCDEV_SIZE + off;
}
if (new_pos < 0) {
return -EINVAL;
} else {
fp->f_pos = new_pos;
return new_pos;
}
}
static long mycdev_unlocked_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
{
long retval = 0;
int err = 0;
int ioarg = 0;
/* 检测命令是否有效 */
if (_IOC_TYPE(cmd) != MYCDEV_IOC_MAGIC) {
return -EINVAL;
} else if (_IOC_NR(cmd) > MYCDEV_IOC_MAXNR) {
return -EINVAL;
}
/* 根据命令检测参数空间是否可访问 */
if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
if (err) return -EFAULT;
/* 执行命令 */
switch(cmd) {
case MYCDEV_IOC_PRINT:
printk("====== CMD:MYCDEV_IOC_PRINT Done ======\n");
break;
case MYCDEV_IOC_GETDATA:
ioarg = 4321;
retval = __put_user(ioarg, (int *)arg);
break;
case MYCDEV_IOC_SETDATA:
retval = __get_user(ioarg, (int *)arg);
printk("====== GETDATA From UserSpace:%d ======\n", ioarg);
break;
default:
return -ENOTTY;
}
return retval;
}
int mycdev_mmap(struct file *flip, struct vm_area_struct *vma)
{
struct mycdev *dev = flip->private_data;
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
if (remap_pfn_range(vma,
vma->vm_start, virt_to_phys(dev->mem)<
vma->vm_end-vma->vm_start, vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}
/* paddingg struct file operation */
static const struct file_operations mycdev_fops = {
.owner = THIS_MODULE,
.read = mycdev_read,
.write = mycdev_write,
.open = mycdev_open,
.release = mycdev_release,
.llseek = mycdev_llseek,
.unlocked_ioctl = mycdev_unlocked_ioctl,
.mmap = mycdev_mmap,
};
static void setup_mycdev(struct mycdev *dev, int index)
{
int ret;
int devno = MKDEV(mycdev_major, index);
cdev_init(&dev->cdev, &mycdev_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &mycdev_fops;
ret = cdev_add(&dev->cdev, devno, 1);
if (ret) {
printk("adding mycdev error!\n");
}
}
static int __init mycdev_init(void)
{
int ret;
if (mycdev_major) { /* 静态分配 */
devno = MKDEV(mycdev_major, 0);
ret = register_chrdev_region(devno, 1, "mycdev");
} else { /* 动态分配 */
ret = alloc_chrdev_region(&devno, 0, 1, "mycdev");
mycdev_major = MAJOR(devno);
}
/* 动态创建设备文件 */
myclass = class_create(THIS_MODULE, "mycdev_driver");
device_create(myclass, NULL, devno, NULL, MYCDEV_NAME);
/* 分配设备空间,等同于cdev_alloc */
devp = kmalloc(sizeof(struct mycdev), GFP_KERNEL);
if (!devp) {
ret = -ENOMEM;
unregister_chrdev_region(devno, 1);
return ret;
}
memset(devp, 0, sizeof(struct mycdev));
/* 初始化等待队列 */
init_waitqueue_head(&devp->inq);
init_waitqueue_head(&devp->outq);
/* 添加并启动设备 */
setup_mycdev(devp, 0);
return 0;
}
static void __exit mycdev_exit(void)
{
printk("mycdev module is leaving...\n");
cdev_del(&devp->cdev);
kfree(devp);
unregister_chrdev_region(MKDEV(mycdev_major, 0), 1);
/* 删除设备文件 */
device_destroy(myclass, devno);
class_destroy(myclass);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
Makefile与“简单字符设备驱动”一节相同,测试文件
/*
* ===================================================================================
* Filename: usr_test.c
*
* Description:
*
* Version: 1.0 (02/26/2013)
* Created: xhzuoxin(xiahouzuoxin@163.com)
* Compiler: gcc
* ===================================================================================
*/
#include
#include
#include
#include
#include
#include
#include "mycdev.h"
int main(void)
{
int testdev;
int n, i, ret;
char write_buf[] = "xiahouzuoxin";
char buf[12];
sprintf(buf, "/dev/%s", MYCDEV_NAME);
testdev = open(buf, O_RDWR);
if (testdev == -1) {
printf("cannot open file.\n");
exit(1);
}
memset(buf, 0, sizeof(buf)/sizeof(char));
n = sizeof(write_buf)/sizeof(char);
if (ret = write(testdev, write_buf, n) < n) {
printf("write error!\n");
exit(1);
}
lseek(testdev, 0, SEEK_SET);
if (ret = read(testdev, buf, n) < n) {
printf("read error!\n");
exit(1);
}
for (i=0; i
printf("%c", buf[i]);
}
printf("\n");
/* test @ioctl */
{
int ioarg = 9000;
if (ioctl(testdev, MYCDEV_IOC_PRINT) < 0) {
printf("error in ioctl: MYCDEV_IOC_PRINT.\n");
return -1;
}
if (ioctl(testdev, MYCDEV_IOC_SETDATA, &ioarg) < 0) {
printf("error in ioctl: MYCDEV_IOC_SETDATA.\n");
return -1;
}
if (ioctl(testdev, MYCDEV_IOC_GETDATA, &ioarg) < 0) {
printf("error in ioctl: MYCDEV_IOC_GETDATA.\n");
return -1;
} else {
printf("GETDATA:%d\n", ioarg);
}
}
close(testdev);
return 0;
}
./usr_test可观察用户空间执行结果,使用tail –n 4 /var/log/message观察驱动程序打印信息
[root@CentOS mycdev]# ./usr_test
xiahouzuoxin
GETDATA:4321
[root@CentOS mycdev]# tail -n 4 /var/log/messages
May 12 13:03:23 CentOS kernel: write 13 bytes from 0
May 12 13:03:23 CentOS kernel: read 13 bytes from 0
May 12 13:03:23 CentOS kernel: ====== CMD:MYCDEV_IOC_PRINT Down ======
May 12 13:03:23 CentOS kernel: ====== GETDATA From UserSpace:9000 ======
[root@CentOS mycdev]#