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. css属性值语法解读

    //margin 形式语法: [ <length> | <percentage> | auto ]{1,4} //合法实例: margin: style /*单值语法 */ 举 ...

  2. SQL点点滴滴_DELETE小计

    惨痛的教训: 某次在执行delete时,一时疏忽忘记写where条件了, 1.删除tb_mobile_cust_micromsg中的内容,前提是c_customer这个字段的值与#datamod表中c ...

  3. Android已上线应用开源分享中(第二季)

    昨天和大家分享了我Android上线的第一个应用,大家还是挺支持的,很高兴,虽然作品没有那么高大上,但是很是有了一点小小的成就感,所以打算继续开源我上线的一些应用,和大家一起交流一下. 我这个作品是一 ...

  4. QT的QCombox

    https://stackoverflow.com/questions/29939990/qcombobox-style-for-choosed-item-in-drop-down-list

  5. ACM HDU-2952 Counting Sheep

    Counting Sheep Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) T ...

  6. Vim快捷输出查找寄存器的内容(去除\<,\>和\V)

    Vim自带的*搜索会自动在单词两头加上\<和\>,使用第三方的vnoremap *,则是加上前缀\V, 当我们想要输出刚刚搜索的内容时可用<C-r>/,但是很可能会带上多余的符 ...

  7. 第一周 day1 Python学习笔记

    为什么要学习Python? Python擅长的领域 1. python2.x中不支持中文编码,默认编码格式为ASCII码,而python3.x中支持Unicode编码,支持中文,变量名可以为中文,如: ...

  8. windows10 如何关闭快速关机功能电源选项

    点击右下角的电池 -> power and sleep setting -> choose what the power buttons do -> change settings ...

  9. 几个python练习题

    从python公众号里面看到了几道python的练习题,就拿来练练手,结果上手了发现自己还是特别水,不是很难的8道题,我只做出来5道,其中还3道题卡住了,边查边做的.原题链接在这里:http://py ...

  10. BZOJ2427:[HAOI2010]软件安装(树形DP,强连通分量)

    Description 现在我们的手头有N个软件,对于一个软件i,它要占用Wi的磁盘空间,它的价值为Vi.我们希望从中选择一些软件安装到一台磁盘容量为M计算机上,使得这些软件的价值尽可能大(即Vi的和 ...