LDD3第三章学习笔记

思维导图

LDD3第三章学习笔记

需求

实现一个设备/dev/scull,这个设备能用dd, cp, cat和Shell的IO重定向功能操作。

设备号

Linux用主次两个设备号去唯一的表示一个设备。其中主设备号表示一类驱动,而次设备号用来给具体的设备编号。内核可以通过此设备号得到一个设备指针

除了主次设备号,内核也使用专门的dev_t类型去给设备编号,dev_t是一个32位的整型变量。其中前12位用来表示主设备号,低20位用来表示次设备号。

设备号的转换

主次设备号和devt_t的转换通过以下三个宏实现:

MAJOR(dev_t dev);   //从设备编号中提取主设备号
MAJOR(dev_t dev);   //从设备编号中提取从设备号
MKDEV(int major, int minor);  //根据主从设备号计算出dev_t类型的设备号

主要的数据结构

(1)struct file

struct file  {
    void * private_data; //驱动程序常用字段,本文使用该字段表示量子集的头指针
    loff_t f_pos; //读写文件的位置表示当前进程读写到了文件的那一个部分
    atomic_long_t f_count; //结构体被进程引用的次数---引用计数
    struct file_operation *f_ops; //表示在该文件上可以执行的操作
};

该结构体表示一个打开的设备或文件,在task_struct中,存在一个打开文件表,表中的每一项都是一个struct file结构体。

(2)struct file_operation

struct file_operation {
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
};

该结构体是一个类似OOP中接口的东西,只定义了一系列操作函数的参数和返回值。具体怎么实现有实现他的结构体去决定。

(3)struct inode

struct inode {
    struct cdev *i_cdev;
    dev_t i_rdev;
};

文件系统中使用inode指向一个文件在磁盘中的位置。但是驱动程序中主要使用的主要是涉及到设备的字段,i_rdev表示设备编号,i_cdev指向一个设备

(4)struct scull_device

struct scull_device {
    struct  semaphore sem; //同步用信号量
    struct cdev; //实现了一个字符设备
    struct qset *data; //链表的头指针
    int size; //设备存储的数据字节数
    int quantum; //量子的大小
    int qset; //指针数组的大小
};

scull设备,在用户态抽象成/dev/scull。

(5)struct cdev

struct cdev {
    struct kobject *kobj; //指向一个kobject类型
    struct module *owner; //表示该设备属于那个厂商
    struct file_operations *ops; //留坑
    struct list_head list; //链接系统内所有的字符设备
    dev_t dev; //设备编号
    unsigned int count;
};

字符设备的注册

(1)cdev_alloc

struct cdev * cdev_alloc (void);

cdev_alloc内部会调用kmalloc给设备分配空间

(2)cdev_init

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

cdev_init主要是用来初始化file_operations结构

(3)cdev_add

int cdev_add (struct cdev * p,
              dev_t dev,
              unsigned count);

把一个设备添加到系统里,需要注意执行这个操作之后,设备会立即生效,设备可以开始读写。

(4)scull中的设备注册

struct scull_device封装了字符设备,因此scull中的字符设备不需要cdev_alloc去分配空间。同时scull的设备注册放到了模块初始化中完成。主要流程为:

  1. cdev_init中使用全局的file_operations结构初始化字符设备

  2. 调用cdev_add把设备添加到系统

open和release

(1) open

进程使用设备需要申请和释放,而open和release就分别用于实现这两个目的。open函数的签名如下:

int (*open)(struct inode *inode, struct file *filp);

它接受两个参数:inode主要用于从获取字符设备指针,filp是文件指针,前文提到的进程的打开文件表中的就是filp

scull中open流程如下:

  1. 通过设备指针inode->i_cdev获取struct scull_dev设备指针,并且让filp的private_data字段指向scull_dev设备指针。
  2. 如果是以只写模式打开设备,还需要调用scull_trim去清空量子集里全部的数据
int(*release)(struct inode *, struct file *)

(2) release

release的主要工作有两个:

a. 释放file指针中的private字段

b. 在最后一次close的时候关闭设备

scull的内存结构

scull设备实际上就是内核内存中一片可读写的区域,这片区域使用链表结构去表示,链表中的每个节点表示一个

独立的读写区域,最小的读写单元称作量子。链表的每个节点称作量子集。

默认情况下,每个量子集存储有1000个量子,每个量子能存储4000字节的数据,一个量子集存储4M数据。

在scull源码中SCULL_QUANTUM表示量子存储的默认字节数,SCULL_QSET表示每个量子集存储的量子个数。

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

struct scull_device中的data字段表示链表的头指针

[此处缺张图]

scull的读写和清空操作

(1) read

1.获取当前进程分配的设备指针

2.计算量子集存储的总字节数,计算方式是量子大小 * 量子集里量子个数

3.进程访问文件的位置比设备大小要大,说明已经读到了EOF,函数返回0

4.如果本次读操作会越界,则缩小count到文件边界

5.使用f_pos换算当前访问到了哪个量子集—除法,并且换算出占用最后一个量子集多少字节的空间

6.使用scull_follow定位到要读取的量子集

7.如果本次会读取会超过最后一个量子的边界,则缩小count

8.从指定位置开始拷贝count字节的数据,更新失败返回-EFAULT

9.更新f_pos和设置函数返回值

【此处缺张图】

定位要读取的量子集

(2) write

1.计算每个量子集的大小,设置函数默认返回值为ENOMEM

2.计算item, rest

3.计算s_pos,q_pos

4.调用scull_follow定位要写入的原子集的位置

5.如果data或者data[s_pos]为空,则给他们分配内存

6.如果写入count字节会导致量子溢出,则修改count

7.执行拷贝操作,失败返回-EFAULT

8.更新f_pos和返回值

9.如果dev->size小于f_pos,更新dev->size为f_pos

参考

1.LDD3

Original: https://www.cnblogs.com/dennis-wong/p/16221835.html
Author: 成蹊0xc000
Title: LDD3第三章学习笔记

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/582766/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球