linux驱动有基本的接口进行注册和卸载,这里不再做详细说明,本文主要关注linux字符设备驱动框架实现的细节。

1.字符设备驱动抽象结构

字符设备驱动管理的核心对象是字符设备,从字符设备驱动程序的设计框架出发,内核为字符设备抽象出数据结构struct cdev,定义如下:

include/linux/cdev.h
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

设备驱动程序中可以有两种方式来产生struct cdev对象,一种是静态定义的方式,另一种是在程序的执行期通过动态分配。而一个struct cdev对象在被加入系统前,应该被初始化,可以通过cdev_init接口完成,具体实现如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}

2.设备号及设备节点

由于struct cdev在添加到系统时需要设备号和设备节点的知识,所以此处先做个简介。

2.1 设备号分配与管理

linux系统中设备号由主设备号和次设备号组成,内核使用主设备号来定位设备驱动程序,用次设备号来管理同类设备。使用dev_t类型变量标记设备号,具体定义如下:

include/linux/types.h
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

在当前版本内核,dev_t的低20位表示次设备号,高12位表示主设备号,具体定义如下:

include/linux/kdev_t.h
#define MINORBITS20
#define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

字符设备涉及到设备号分配的内核函数

fs/char_dev.c
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name);
if (IS_ERR(cd))
goto fail;
} return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}

该函数第一个参数表示设备号,第二个参数表示连续设备编号个数,即驱动管理的同类设备个数,第三个参数表示设备的名称,该函数的核心是__register_chrdev_region,讨论该函数前需要先看下全局指针数组chrdevs,这个数组每一项都指向一个struct char_device_struct结构,具体定义如下:

static struct char_device_struct
{
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev;/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

__register_chrdev_region将当前设备驱动程序使用的设备号记录到这个数组中,首先分配一个struct char_device_struct的对象,然后对其初始化,完成后哈希遍历char devs并将这个对象添加到数组中。 在对字符设备初始化后,需要将该设备添加到内核当中,可以查看cdev_add函数定义。

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error; p->dev = dev;
p->count = count; error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent); return 0;
}

cdev_add函数的核心是通过kobj_map实现,后者通过操作一个全局变量cdev_map把设备加入到哈希表中,cdev_map定义如下: static struct kobj_map *cdev_map;

struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct mutex *lock;
};

简单的说,设备驱动程序通过调用cdev_add把它所管理的设备对象指针嵌入到一个struct probe节点对象中,然后再把该节点加入到cdev_map的哈希链表中。

2.2 设备节点的生成

设备节点的创建可以使用mknod指令在/dev目录下进行创建,例如mknod /dev/demodev c 2 0,如果该指令成功执行,将会在/dev目录下生成一个demodev的字符设备节点。

而mknod实际上是通过系统调用sys_mknod进入内核空间,函数的原型asmlinkage long sys_mknod(const char __user *filename, umode_t mode, unsigned dev);

mknod首先在根目录下寻找dev目录所对应的inode,通过inode编号得到该inode的内存地址,然后通过dev的inode结构中的i_op成员指针(ext3_dir_inode_operations),来调用该对象的mknod方法,这将导致ext3_mknod函数被调用。

ext3_mknod函数会创建一个新的inode节点,然后调用init_special_inode函数与设备相关联,具体实现如下:

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_fops;
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"" inode %s:%lu\n", mode, inode->i_sb->s_id,inode->i_ino);
}

这个函数主要是初始化inode的成员i_fop和i_rdev,其中i_rdev成员表示该inode所对应设备的设备号。

3.打开设备文件

假设已经实现了/dev/demodev的创建,我们再来看下用户空间open函数是如何打开设备文件的。

首先看下linux用户空间open函数原型,详细说明可以使用man 2 open查看,其定义int open(const char pathname, int flags, mode_t mode);

而struct file_operations结构中对应open函数原型int (*open) (struct inode *, struct file *)两者差别很大,那么用户态的open接口是如何一步步的调用到内核态的open函数呢?首先用户空间的open会产生系统调用,通过sys_open进入内核空间,而sys_open函数声明如下:

asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);

但是源码中搜寻不到sys_open函数实现,可以通过”SYSCALL_DEFINE3(open”搜索源代码,感兴趣的同学可以把这个宏展开,这里不再赘述,看下函数定义:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE; return do_sys_open(AT_FDCWD, filename, flags, mode);
} //很明显do_sys_open才是代码核心,接着来看:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int lookup = build_open_flags(flags, mode, &op);
struct filename *tmp = getname(filename);
int fd = PTR_ERR(tmp); if (!IS_ERR(tmp)) {
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op, lookup);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
} return fd;
}

其中do_sys_open首先会调用get_unused_fd_flags为这次open操作分配一个文件描述符fd,get_unused_fd_flags实际上是对alloc_fd的封装。随后do_sys_open调用do_filp_open函数,后者会查询/dev/demodev设备文件inode,将inode结构中i_fop赋值给filp->f_op,然后调用i_fop中的open函数。在设备节点的生成时,inode->i_fop = &def_chr_fops,所以open函数直接调用chrdev_open。

const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
}; static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0; spin_lock(&cdev_lock);
p = inode->i_cdev; if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret; ret = -ENXIO;
filp->f_op = fops_get(p->ops);
if (!filp->f_op)
goto out_cdev_put; if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);
if (ret)
goto out_cdev_put;
} return 0; out_cdev_put:
cdev_put(p);
return ret;
}

chrdev_open通过调用kobj_lookup在cdev_map中用inode->i_rdev来查找设备号对应的字符设备,成功找到设备后,通过filp->f_op = fops_get(p->ops)将cdev对象的ops赋值给file对象的filp成员,同时会把cdev对象保存到inode->i_cdev成员中,这时已经将file和file_operations关联起来。

linux学习--字符设备驱动的更多相关文章

  1. Linux实现字符设备驱动的基础步骤

    Linux应用层想要操作kernel层的API,比方想操作相关GPIO或寄存器,能够通过写一个字符设备驱动来实现. 1.先在rootfs中的 /dev/ 下生成一个字符设备.注意主设备号 和 从设备号 ...

  2. linux driver ------ 字符设备驱动 之 “ 创建设备节点流程 ”

    在字符设备驱动开发的入门教程中,最常见的就是用device_create()函数来创建设备节点了,但是在之后阅读内核源码的过程中却很少见device_create()的踪影了,取而代之的是device ...

  3. Linux高级字符设备驱动

    转载:http://www.linuxidc.com/Linux/2012-05/60469p4.htm 1.什么是Poll方法,功能是什么? 2.Select系统调用(功能)      Select ...

  4. linux 高级字符设备驱动 ioctl操作介绍 例程分析实现【转】

    转自:http://my.oschina.net/u/274829/blog/285014 1,ioctl介绍 ioctl控制设备读写数据以及关闭等. 用户空间函数原型:int ioctl(int f ...

  5. Linux高级字符设备驱动 poll方法(select多路监控原理与实现)

    1.什么是Poll方法,功能是什么? 2.Select系统调用(功能)      Select系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程.      int selec ...

  6. Linux LED字符设备驱动

    // 申请IO资源 int gpio_request(unsigned gpio, const char *label); // 释放IO资源 void gpio_free(unsigned gpio ...

  7. Linux 简单字符设备驱动

    1.hello_drv.c (1) 初始化和卸载函数的格式是固定的,函数名自定义 (2) printk是内核的打印函数,用法与printf一致 (3) MODULE_LICENSE:模块代码支持开源协 ...

  8. linux字符设备驱动学习笔记(一):简单的字符设备驱动

    最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...

  9. 嵌入式Linux驱动学习之路(十)字符设备驱动-my_led

    首先贴上代码: 字符设备驱动代码: /** *file name: led.c */#include <linux/sched.h> #include <linux/signal.h ...

随机推荐

  1. js前台实现上传图片的预览

    网上这样的插件一大堆,不过还是谈下js下代码的实现,加深这方面的理解. 当然也没有一种方式就可以完事的情形,主要就两种方面来处理: 1.file API的filereader接口完成(支持的浏览器:I ...

  2. Web API 2 入门——Web API 2(C#)入门(谷歌翻译)

    ASP.NET Web API 2(C#)入门 在这篇文章中 本教程中使用的软件版本 创建一个Web API项目 添加模型 添加控制器 使用Javascript和jQuery调用Web API 运行应 ...

  3. 【Leetcode】【Medium】Best Time to Buy and Sell Stock II

    Say you have an array for which the ith element is the price of a given stock on day i. Design an al ...

  4. sql 嵌套查询

    sql 某一字段 数量大于1 SELECT * FROM ecm_goods_spec AWHERE ( SELECT count( * ) FROM ecm_goods_spec BWHERE A. ...

  5. [原]如何在Android用FFmpeg+SDL2.0之同步视频

    关于视频同步的原理可以参考http://dranger.com/ffmpeg/tutorial05.html 和 [原]基础学习视频解码之同步视频 这两篇文章,本文是在这两篇的基础上移植到了Andro ...

  6. websphere8 从安装到部署 测试集群应用程序 安装j2ee程序(非常详细)

    目录1. 准备安装文件2. 安装Installation Manager3. 为Installation Manager指定安装资源库4. 创建部署管理器概要文件5. 创建定制概要文件并联合到部署管理 ...

  7. Nucleus进程间通信(IPC)方式

    版权声明:本文为博主原创文章,未经博主同意不得转载--"http://blog.csdn.net/suipingsp". https://blog.csdn.net/suiping ...

  8. 常用的css选择器

    1.最基本的 * * 选择所有元素. #id #firstname 选择 id="firstname" 的元素. .class .intro 选择 class="intr ...

  9. Selenium应用代码(读取excel的内容进行注册的案例)

    1. 封装读取excel数据的方法:import java.io.*;import java.util.ArrayList;import java.util.List; import jxl.*;im ...

  10. 二十九、利用 IntelliJ IDEA 进行代码对比的方法

    我们会有这样的需求,即:想对比出两个不同版本代码的区别.如何实现? 第 1 种:如果我们是从 SVN 检出的项目,并且想比较本地代码与从 SVN 检出时的代码相比都有那些区别,可以按如下步骤操作, 如 ...