linux kernel 字符设备详解

有关Linux kernel 字符设备分析:

参考:http://blog.jobbole.com/86531/

一.linux kernel 将设备分为3大类,字符设备,块设备,网络设备.

字符设备是指只能一个字节一个字节读写的设备, 常见的外设基本上都是字符设备.

块设备:常见的存储设备,硬盘,SD卡都归为块设备,块设备是按一块一块读取的.

网络设备:linux 将对外通信的一个机制抽象成一个设备, 通过套接字对其进行相关的操作.

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

二、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系。

三.字符设备的模型

四.下面讲一下字符设备驱动的编写流程,linux 内核为字符设备的创建提供了一套接口.

首先介绍一下dev_t , 他是主设备号和次设备号的结构体生成,他就代表了一个主次设备号

通过函数MKDEV (MAJ , MINOR) ; 生成.我们注册一个字符设备可以通过动态注册也可以静态注册 ,  linux kernel 为我们提供了所需要的接口

首先讲一下静态注册的方法

 1     //分配主设备号为200  次设备号从5开始分配5个  设备名字叫MONEY
 2     int ret ;
 3     DeviceId = MKDEV(MAJ , BASEMINOR);
 4     ret = register_chrdev_region(DeviceId , MINORCNT , "MONEY");
 5     if(ret < 0)
 6     {
 7         return ret ;
 8     }
 9
10     //********************************************
11     //方法一
12     //1> 初始化
13     cdev_init(&device, &fops);
14     //2> 添加     domain->probes  HASH表上
15     cdev_add(&device,DeviceId , MINORCNT);                                      

通过函数register_chrdev_region() , 我们可以注册主设备号为MAJ , 次设备号BASEMINOR 开始 ,  一共注册MINORCNT 个次设备号 , 名字为MONEY 的字符设备.

第二种方法是动态申请主次设备号:

 1     //动态分配一个主设备号
 2     int ret ;
 3     ret = alloc_chrdev_region(&DeviceId , BASEMINOR , MINORCNT,"TONY");
 4     if(ret < 0)
 5     {
 6         return ret ;
 7     }
 8
 9     printk("major:%d \n" , MAJOR(DeviceId));
10
11     //********************************************
12     //方法二
13     //1> 分配空间
14     device = cdev_alloc();                                                      

我们可以通过linux kernel 提供的alloc_chrdev_region () 的方法 , 申请一个主设备号和基础设备号 , 一共申请 MINORCNT , 名字为 TONY 的一个字符设备.

这里涉及一个结构体:

1 struct cdev {
2     struct kobject kobj;
3     struct module *owner;        4     const struct file_operations *ops;
5     struct list_head list;
6     dev_t dev;
7     unsigned int count;
8 };                                     

这里的话还要申请一个cdev 结构体的空间

通过cdev_alloc() ;

搞定了主次设备号的问题 , 接下来就是涉及到了初始化和添加到设备列表 .

linux kernel 为我们提供了以下的方法:

1     //2> 初始化
2     cdev_init(device, &fops);
3     //3> 添加     domain->probes  HASH表上
4     cdev_add(device,DeviceId , MINORCNT);
5                                                                                 

这里面涉及到了一个&fops 的文件操作结构体

1 static struct file_operations  fops = {
2     .owner = THIS_MODULE,
3     .open = myopen,
4     .read = myread ,
5     .write = mywrite,
6     .release = myclose,
7     .unlocked_ioctl = myioctl,
8 };

上层的open read write 等函数经过一系列的转换都会到对应的函数

相对应的, 释放主次设备号, 删除在设备列表的节点, linux kernel 为我们提供了一下接口:

1     cdev_del(device);
2
3     unregister_chrdev_region(DeviceId , MINORCNT);

下面的代码是想深入了解里面的代码的看看就行了 , 如果只是向了解接口的 , 上面的就行了

从静态申请注册开始跟起吧

1 #define MKDEV(ma,mi)    ((ma)<<8 | (mi))

上面这个是制作一个主次设备号的结构体

1 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
2 extern int register_chrdev_region(dev_t, unsigned, const char *);
3 extern int __register_chrdev(unsigned int major, unsigned int baseminor,
4                  unsigned int count, const char *name,
5                  const struct file_operations *fops);
6 extern void __unregister_chrdev(unsigned int major, unsigned int baseminor,                    unsigned int count, const char *name);
7 extern void unregister_chrdev_region(dev_t, unsigned);     

这是几个将要用的函数的函数声明  ,  它在 include/linux/fs.h 文件中

首先看一下 register_chrdev_region() 函数

1 /**
2  * register_chrdev_region() - register a range of device numbers
3  * @from: the first in the desired range of device numbers; must include
4  *        the major number.
5  * @count: the number of consecutive device numbers required
6  * @name: the name of the device or driver.
7  *
8  * Return value is zero on success, a negative error code on failure.
9  */                     

注释说明:  注册一个范围的设备号 , 原型如下:

 1 int register_chrdev_region(dev_t from, unsigned count, const char *name)
 2 {
 3     struct char_device_struct *cd;
 4     dev_t to = from + count;
 5     dev_t n, next;
 6
 7     for (n = from; n < to; n = next) {                                                 next = MKDEV(MAJOR(n)+1, 0);
 8         if (next > to)
 9             next = to;
10         cd = __register_chrdev_region(MAJOR(n), MINOR(n),
11                    next - n, name);
12         if (IS_ERR(cd))
13             goto fail;
14     }
15     return 0;
16 fail:
17     to = n;
18     for (n = from; n < to; n = next) {
19         next = MKDEV(MAJOR(n)+1, 0);
20         kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
21     }
22     return PTR_ERR(cd);
23 }

它是调用了

      cd = __register_chrdev_region(MAJOR(n), MINOR(n),    

进里面看看

 1 /*
 2  * Register a single major with a specified minor range.
 3  *
 4  * If major == 0 this functions will dynamically allocate a major and return
 5  * its number.
 6  *
 7  * If major > 0 this function will attempt to reserve the passed range of
 8  * minors and will return zero on success.
 9  *
10  * Returns a -ve errno on failure.
11  */

还是看注释: 注册一个指定的主设备号 和一个指定的次设备号范围

判断 主设备号是不是为零 , 如果是零的话 就动态申请一个主设备号 , 这就是后面要讲的那个动态申请 , 它也是调用了这个.

代码如下

 1 static struct char_device_struct *
 2 __register_chrdev_region(unsigned int major, unsigned int baseminor,
 3                int minorct, const char *name)
 4 {
 5     struct char_device_struct *cd, **cp;
 6     int ret = 0;
 7     int i;
 8  这里面涉及一个结构体没讲:
1 static struct char_device_struct {
2     struct char_device_struct *next;
3     unsigned int major;
4     unsigned int baseminor;
5     int minorct;
6     char name[64];
7     struct cdev *cdev;      /* will die */
8 } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
 9     cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);     //动态申请了一个char_device_struct 结构体
10     if (cd == NULL)
11         return ERR_PTR(-ENOMEM);
12
13     mutex_lock(&chrdevs_lock);      //加一个互斥锁 , 防止其他进程并发
14
15     /* temporary */
16     if (major == 0) {
17         for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {       //这里其实就是做了一个动态申请主设备号的功能
18             if (chrdevs[i] == NULL)
19                 break;
20         }
21
22         if (i == 0) {      //没有申请到主设备号, 直接退出
23             ret = -EBUSY;
24             goto out;
25         }
26         major = i;
27         ret = major;
28     }
29
30     cd->major = major;                  //对结构体进行初始化
31     cd->baseminor = baseminor;
32     cd->minorct = minorct;
33     strlcpy(cd->name, name, sizeof(cd->name));
34
35     i = major_to_index(major);      //  哈希表的下标生成
36
37     for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)    //进入chdevs[i] 哈希表快速进入
38         if ((*cp)->major > major ||
39             ((*cp)->major == major &&
40              (((*cp)->baseminor >= baseminor) ||
41               ((*cp)->baseminor + (*cp)->minorct > baseminor))))
42             break;
43
44     /* Check for overlapping minor ranges.  */      // 检查次设备号会不会重叠
45     if (*cp && (*cp)->major == major) {
46         int old_min = (*cp)->baseminor;
47         int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
48         int new_min = baseminor;
49         int new_max = baseminor + minorct - 1;
50
51         /* New driver overlaps from the left.  */
52         if (new_max >= old_min && new_max <= old_max) {
53             ret = -EBUSY;
54             goto out;
55         }
56
57         /* New driver overlaps from the right.  */
58         if (new_min <= old_max && new_min >= old_min) {
59             ret = -EBUSY;
60             goto out;
61         }
62     }
63
64     cd->next = *cp;
65     *cp = cd;
66     mutex_unlock(&chrdevs_lock);      //解锁
67     return cd;
68 out:
69     mutex_unlock(&chrdevs_lock);
70     kfree(cd);
71     return ERR_PTR(ret);
72 }

到这里一个主次设备号就搞定了 , 并申请一个char_device_struct 结构体, 并对其进行赋值初始化.

第二步就是对cdev  进行init :

1 void cdev_init(struct cdev *, const struct file_operations *);    

这里又涉及到一个struct cdev 的结构体:

1 struct cdev {
2     struct kobject kobj;
3     struct module *owner;
4     const struct file_operations *ops;
5     struct list_head list;
6     dev_t dev;
7     unsigned int count;
8 };                                                    

进初始化代码一看究竟:

 1 /**
 2  * cdev_init() - initialize a cdev structure
 3  * @cdev: the structure to initialize
 4  * @fops: the file_operations for this device
 5  *
 6  * Initializes @cdev, remembering @fops, making it ready to add to the
 7  * system with cdev_add().
 8  */
 9 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
10 {
11     memset(cdev, 0, sizeof *cdev);
12     INIT_LIST_HEAD(&cdev->list);
13     kobject_init(&cdev->kobj, &ktype_cdev_default);
14     cdev->ops = fops;
15 }                                                     

看一段代码之前我们尽可能的先看注释, 这样会让我们跟代码轻松很多 , 我们可以顺着代码的作者的思路走

注释: 初始化一个cdev  结构体

但是跟都这里就不能跟进去了 , 因为是要添加到kernel list 当中 , 针对这个东西 , 我到时候会做一个详细的详细的讲解 , 我们理解它是把cdev  挂到内核链表中就行了

下一部就是 cdev_add () ;

1 int cdev_add(struct cdev *, dev_t, unsigned);                           
时间: 2024-12-26 08:31:32

linux kernel 字符设备详解的相关文章

linux系统字符编码详解

众所周知,地球上的语言多种多样,在计算机世界,自然也是要适应各种语言.我们安装各种系统的时候也是明示了要选择语言环境和支持的语言环境. 而linux系统的字符编码设置尤为复杂,这可能也是没有考虑到非技术人员去研究这些东西吧. 我遇到的事情是这样的,我们使用了docker,但是docker容器里的语言环境经常莫名错乱,搞得很头痛,所以偶尔就要切换,或者生成其他字符集.所以现在假设我们需要切换一个中文语言环境,而切换了之后是乱码的,也就是没用的. 基本上,乱码的原因就是没加载到合适的字符编码环境,即

(转)Linux下PS命令详解

(转)Linux下PS命令详解 整理自:http://blog.chinaunix.net/space.php?uid=20564848&do=blog&id=74654 要对系统中进程进行监测控制,查看状态,内存,CPU的使用情况,使用命令:/bin/ps (1) ps :是显示瞬间进程的状态,并不动态连续: (2) top:如果想对进程运行时间监控,应该用 top 命令: (3) kill 用于杀死进程或者给进程发送信号: (4) 查看文章最后的man手册,可以查看ps的每项输出的含义

Linux笔记Linux 系统命令及其使用详解(大全)

开机默认界面修改:字符界面和图形界面-->修改ect/inittab文件 Windows远程Telnet访问Linux系统:telnet+远程Linux系统IP地址 Linux目录结构: /:根目录 /bin:存放必要的命令 /boot:存放内核以及启动所需的文件等 /dev:存放设备文件 /etc:存放系统的配置文件 /home:用户文件的主目录,用户数据存放在其主目录中 /lib:存放必要的运行库 /mnt:存放临时的映射文件系统 /proc:存放存储进程和系统信息 /root:超级用户的主

linux中ls命令详解

s 命令可以说是linux下最常用的命令之一. -a 列出目录下的所有文件,包括以 . 开头的隐含文件.-b 把文件名中不可输出的字符用反斜杠加字符编号(就象在C语言里一样)的形式列出.-c 输出文件的 i 节点的修改时间,并以此排序.-d 将目录象文件一样显示,而不是显示其下的文件.-e 输出时间的全部信息,而不是输出简略信息.-f -U 对输出的文件不排序.-g 无用.-i 输出文件的 i 节点的索引信息.-k 以 k 字节的形式表示文件的大小.-l 列出文件的详细信息.-m 横向输出文件名

Linux开机启动程序详解

我们假设大家已经熟悉其它操作系统的引导过程,了解硬件的自检引导步骤,就只从Linux操作系统的引导加载程序(对个人电脑而言通常是LILO)开始,介绍Linux开机引导的步骤. 加载内核LILO启动之后,如果你选择了Linux作为准备引导的操作系统,第一个被加载的东西就是内核.请记住此时的计算机内存中还不存在任何操作系统,PC(因为它们天然的设计缺陷)也还没有办法存取机器上全部的内存.因此,内核就必须完整地加载到可用RAM的第一个兆字节之内.为了实现这个目的,内核是被压缩了的.这个文件的头部包含着

Linux的i2c驱动详解

目录(?)[-] 简介 架构 设备注册 I2C关键数据结构和详细注册流程 关键数据结构 详细注册流程 使用I2C子系统资源函数操作I2C设备 Gpio模拟i2c总线的通用传输算法 总结 理清i2c中的个结构体关系 i2c驱动的编写建议 1 简介 I2C 总线仅仅使用 SCL . SDA 两根信号线就实现了设备之间的数据交互,极大地简化对硬件资源和 PCB 板布线空间的占用.因此, I2C 总线被非常广泛地应用在 EEPROM .实时钟.小型 LCD 等设备与 CPU 的接口中. Linux I2

Linux的SOCKET编程详解(转)

Linux的SOCKET编程详解 1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统.由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进 程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如 UNIX BSD有:管道(pipe).命名管道(named pipe)软中断信号(signal) UNIX system V有:消息(message).共享存储区(shared memory)和信号量(semaphore)等. 他们都仅限于用在本机进程之间通信.网间进

Linux根文件系统的详解

                            Linux根文件系统的详解    多数的Linux版本使用的是FHS文件组织结构,FHS是Filesystem Hierarchy Standard(文件系统目录标准)的缩写,其采用树形结构组织文件.实际上FHS仅是规范在根目录(/)下面各个主要目录应该放什么样的文件.然后下面我们就进行对Linux的rootfs进行简单的分析说明.         首先对rootfs进行一下说明,rootfs是Root File System的缩写,表:L

【转】linux中inittab文件详解

原文网址:http://www.2cto.com/os/201108/98426.html linux中inittab文件详解 init的进程号是1(ps -aux | less),从这一点就能看出,init进程是系统所有进程的起点,Linux在完成核内引导以后,就开始运行init程序. init程序需要读取配置文件/etc/inittab.inittab是一个不可执行的文本文件,它有若干行指令所组成. 理解Runlevel: runlevel用来表示在init进程结束之后的系统状态,在系统的硬