Linux设备驱动程序 三 字符设备驱动

Linux设备驱动程序 三 字符设备驱动 笔记

 

第三章 字符驱动设备

 

本章会编写一个完整的字符设备,字符设备简单,易于理解,

名字是scull:Simple Caracter Utility for Loading Localities,区域装载的简单字符工具,

 

scull是一个操作内存区域的字符设备driver,这片内存区域就相当于一个设备。

它不和硬件相关,只是操作从内核分配的一些内存

 

1,scull的设计

第一步,定义driver为用户提供的能力,也就是机制

 

我们的设备是内存,他可以是顺序或者随机存取设备,可以是一个或者多个设备,

 

我们实现了若干设备抽象,每个都有自己的特点,

由模块实现的每种设备称为一种 类型 :

scull0 ~ scull3

这四个设备分别由一个全局且持久的内存区域组成,

全局:若device被多次打开,则打开它的所有fd可共享这个device的数据

持久:如果devicce关闭再打开,data不会丢。

可以用常用命令访问和测试这个device,如cp,cat和shell的I/O重定向等

 

scullpipe0 ~ scullpipe3

这4个FIFO先入先出 设备与管道类似。一个进程读取另一个进程写入的数据。

若多个进程读取同一个device,他们会为数据发送竞争。

scullpipe的内部实现说明在不借助中断的情况下,如何实现阻塞式和非阻塞式 的 读写操作。

虽然实际的driver使用 硬件的中断与他们的drivce保持同步,

但阻塞式和非阻塞式操作是重要的内容,有别于中断处理

 

scullsingle

scullpriv

sculliud

scullwiud

这些设备与scull0类似,但是何时允许open操作方面有些限制。

scullsingle一次只允许一个进程使用这个driver,

scullpriv对每个虚拟控制台(或X终端会话)是私有的,这是因为每个控制台/终端上的进程将获取不同的内存区。

sculluid和scullwuid可以被多次打开,但是每次只能由一个用户打开;

如果另一个用户锁定了device,sculluid返回Device Busy error,

而scullwiud实现了阻塞式open。

这些scull设备的变种混淆了机制和策略,但是值得了解,很多真正的设备需要类似的管理方式

 

本章讲scull0~scull3的内部结构,更复杂的在第六章将,scullpipe在 阻塞式I/O 讲,其他在在 设备文件的访问控制 讲。

 

2,主设备号和次设备号

对字符设备的访问是通过文件系统内的设备名称进行的。被称为特殊文件,设备文件,文件系统树的节点,

它们位于/dev/

 

ls -l时,字符设备 在第一列用 c 识别。也可以看到主次设备号,

主设备号代表device对应的driver。一般一个主设备号对应一个driver

 

次设备号由内核使用,用于正确确定设备文件所指的设备,

我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当做设备本地数组的索引。

 

3,设备编号的内部表达

dev_t(lnux/type.h)保存设备编号,包括主设备号和次设备号

dev_t是32bit,12bit代表主设备号,其余20bit代表次设备号,

 

获取方式:

MAJOR(dev_t dev);

MINOR(dev_t dev);

 

反向操作:

MKDEV(int major, int minor);

 

4,分配和释放设备编号

建立char deivce前, 先要获得一个或者多个设备编号。

通过register_chrdev_region,它在linux/fs.h 声明:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

 

first是要分配的设备编号的范围的起始值,可以给0

count是请求的连续设备编号的个数

name是设备名称,出现在/proc/devices和sysfs中

和多数内核函数一样,成功return 0 ,

 

如果不知道设备要使用的主设备号,需要动态分配:

int alloc_chrdev_region(dev_t *dev, unsigned int firestminor, uint count, cahr *name);

 

dev用于输出,调用成功后保存分配的第一个编号。

firstminor可以给0,

 

释放设备编号:

void unregister_chrdev_region(dev_t first, unisnged int count);

 

一般在清除函数调用它

 

有了设备编号,AP可以访问它,driver需要将设备编号与内部函数连接起来,内部函数实现设备的操作。

 

5,动态分配主设备号

尽量用动态分配,分配了设备号,就可以通过/proc/.devices/和/sys/中读取得到

 

所以insmod可以替换为一个简单的脚本

脚本调用insmod后读取/proc/devices来获得新分配的主设备号,然后创建对应的设备文件。

可以利用awk从/proc/devices获取信息,在/dev/创建设备文件

130|console:/ # cat /proc/devices                                              
Character devices:
  1 mem
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
 10 misc
....
180 usb
188 ttyUSB
189 usb_device
216 rfcomm
227 s3gcard
244 roccat
245 hidraw
246 rtk_btusb
247 zxtz
248 bsg
249 watchdog
250 ptp
251 pps
252 media
253 rtc
254 tee

Block devices:
  1 ramdisk
259 blkext
  7 loop
....
134 sd
135 sd
179 mmc
254 device-mapper

 

脚本:

device="scull"

/sbin/insmod ./$module.ko $* || exit 1

#删除原有节点
rm -f /dev/${device}[0-3] 

major=$(awk "\$2= =\"$module\" {print \$1} /proc/devices)

mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

。。。。

 

6,一些重要的数据结构

三个重要的内核数据结构:

file_oerations,file,inode

在编写真正的drvier前,要对他们有个基本的认识

 

7,file_operations结构体

上面我们保留了设备编号,但是没有与driver建立连接。

file_operations就是用来建立连接的,定义在linux/fd.h,包含了一组函数指针。

每个打开的文件(内部用一个file表示)和一组函数关联(通过包含指向file_operations的f_op字段)

这些操作主要用来实现系统调用如open,read等,

 

可以认为文件是一个对象,操作它的函数是方法,用oop的术语:对象声明的动作将作用于其本身。

这是linux内核看到的面向对象编程的第一个例子,后面更多。

 

按照惯例,file_operations结构或者指向这类结构的指针称为fops,

这个结构中的每个字段必须指向驱动程序中实现特定操作的函数,不支持就置null

 

通读file_operations方法的清单时,会看到许多参数包含__user字符传,它表明指针是一个用户空间的地址,不能被直接引用。

 

struct module *owner

 

unsigned int (*poll) (strcut file *, struct poll_table_struct *);

poll方法是poll,epoll,select三个系统调用的后端实现,他们可以用来查询某个或者多个fd上读取或写入是否会被阻塞。

poll返回一个位掩码,指出非阻塞的读取或者写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。

如果poll置null,device被认为可读可写不会阻塞,、

 

int (*mmap)(struct file *, struct vm_area_struct *);

mmap用于请求将设备内存映射到进程地址空间,

 

scull device driver程序实现的只是最重要的设备方法,file_operations结构init为:

struct file_operations scull_fops = {
    .owner = THIS_MODULE,
    ...
    .read = scull_read,
    .ioctl = scull_ioctl,
    .open = scull_open,
    .realse = scull_release,
}

 

这个声明采用标准c的标记化结构初始化语法,

这种语法值得采用,因为它让driver在结构的定义发生变化时更有可移植性,使代码更紧凑和易读。

标记化的init方法允许对结构成员重新排列,

某些场合,把频繁 被访问的成员放在相同的硬件缓存行,可大大提高性能

 

 

8,file结构体

struct file是driver的第二个重要的数据结构, 定义在linux/fs.h

file结构与UMD的FILE没有任何关联。FILE是C库定义的,不会出现在内核的代码。

 

file结构代表一个打开的文件,不仅driver,每个打开的文件在KMD都对应一个file结构,

内核在open时创建它,并传递给在这个文件上进行操作的所有函数,

 

内核源码中,指向struct file的指针叫filp(文件指针),

 

struct file的重要成员:

mode_t f_mode,文件模式

struct file_operations *f_op; //与文件相关的操作

内核在open时赋值这个指针,以后需要处理这些操作时读这个指针,

filp->f_op的值不会为方便引用而保存起来,。。。。

这里提到了,这种替换文件操作的能力在OOP里叫 方法重载

void *private_data;

private_data是跨系统调用时保存状态信息的非常有用的资源,

记得要在内核销毁file前在release方法里释放它的内存

 

9,inode结构体

内核用inode结构体在内部表示文件,它与file不同,file是打开的文件描述符。

 

有用字段:

dev_t i_rdev;

包含了真正的设备编号

 

struct cdev *i_cdev;

cdev表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,

这个字段包含了指向struct cdev的指针

 

10,字符设备的注册

内核内部用struct cdev表示字符设备,

linux/cdev.h

 

//分配和init cdev有两种方法,
//如果要在运行时获取一个独立的cdev结构:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
//可以把cdev结构嵌入自己的设备特定结构中,scull就是这样做的,
//这时,必须用下面的代码去init
void cdev_init(struct cdev *cdev, struct file_operations *fops);

//告诉内核改结构的信息
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

//Scull的设备注册
//scull通过struct scull_dev的结构表示每一个设备,结构定义如下:
struct scull_dev {
    struct scull_qset *data; //指向第一个量子集的指针,
    int quantum;  //当前量子的大小
    int qset; //当前数组的大小
    unisgned long size; //数据总量
    unsigned int access_key; 
    struct semaphore sem; //互斥信号量
    struct cdev cdev;  //字符设备结构
}

//现在我们集中注意力在cdev,即内核和设备之间的接口struct cdev.
//struct cdev必须如上述被init并添加到系统,
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor+index);
    
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULKE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add(&dev->cdev, devno, 1);
    if(err)
        printk(KERN_NOTICE"Error %d\n",err);
}
因为cdev被嵌入了struct scull_dev中,所以必须调用cdev_init执行该结构的初始化,

 

11,早期的方法

int register_chrdev(unsigned int major, const char *name, struct file_opertions *fops);

它会为给定的主设备号注册0~255次设备号,并为每个设备建立一个对应的默认cdev结构。

name是driver的名称,出现在/proc/devices中,

 

s3g的init方法:

new_kernel/linux/s3.c
static int __init s3g_init(void)
{
    //#define S3G_DEV_NAME "s3gcard"  @s3g_ioctl.h
    //#define S3G_PROC_NAME "driver/s3g"
    ret = register_chrdev(S3G_MAJOR, S3G_DEV_NAME, &s3g_fops);
    
    //struct class *s3g_class
    s3g_class =  class_create(THIS_MODULE, S3G_DEV_NAME);
    
    //初始化s3g_card[0]全局变量,通过s3g_card_init(s3g, NULL)
    //里面也会创建proc节点
    ret = s3g_register_driver();
}

//linux/s3g_drvier.c
int s3g_card_init(s3g_card_t *s3g, void *pdev)
{
    proc_create_data(S3G_PROC_NAME, 0, NULL, &s3g_proc_fops, s3g);
}

static const struct file)operations s3g_proc_fops = 
{
    .owner = THIS_MODULE,
    .read = s3g_proc_read,
    .write = s3g_proc_write,
};

 

12,open和release

看看file_operations的的字段如何使用,

 

open需要完成的工作:

1,检查设备特定错误,如硬件没有就绪

2,若device首次打开,init它

3,若有必要,更新f_op指针

4,分配并填写置于flip->private_data的数据结构

 

 

s3.c有一个file_operations,linux_fb为什么没有?是因为使用了linux内核的接口吗?

s3.h include了所有需要的内核的头文件
 

//new_kernel/linux/s3.h
#ifdef __KERNEL__
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

 

open的原型:

int (*open)(struct inode *inode, struct file *flip)

 

inode在i_cdev字段包含了我们需要的信息,就是我们之前设置的cdev结构,/

然而我们通常不需要cdev,而是需要包含cdev的scull_dev结构,

C语言的一些技巧可以完成这类转换,但不该滥用,因为难以理解,幸好内核黑客已经实现了这个技巧,

 

 

//linux/kernel.h

contaner_of(pointer, container_type, contaner_field);

 

它需要一个contaner_field字段的指针,它包含在container_type类型的结构中,

返回包含该字段的结构指针,

 

在scull_open中,这个宏用来找到适当的设备结构,

struct scull_dev *dev;

dev = container_of(inode->i_cdev, struct scull_dev, cdev);

参数:cdev的指针,包含cdev字段的结构体,cdev字段。

 

找到scull_dev结构后,保存到file结构的private_data字段,方便以后对该指针的访问。

略微简化的scull_open代码:

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev;
    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev;
    
    //trim to 0.....if open was write-onlyu
    if( flip->f_flags == O_WRONLY) {
        scull_trim(dev);
    }
    return 0; //success
}

这段代码很小,因为没有针对某个特定device的处理,

scull设备是全局且持久的,所以不用特别处理。

而且不维护scull的打开计数,值维护模块的使用计数,也就没有“首次打开init device”的动作,

 

对device唯一的操作是,如果以写方式打开,长度就截断为0,。

因为当用更短的文件覆盖一个scull设备时,设备数据区应该缩小,

就像写文件打开时,长度截断为0,

 

如何打开s3gcard driver并使用它?

打开用open,其余操作全在ioctl

//libkeinterface/s3g_keinterface.c
int s3gOpenMinor(int minor)
{
    char path[64];
    sprintf(path, "%s%s%d", S3G_DIR_NAME, S3G_DEV_NAME, minor);
    if((fd = open(path, O_RDWR, 0)) >= 0)
    {
        return fd;
    }
    return -errno;
}


//gralloc/gralloc_s3g.c
__attribute_((constructor)) void gralloc_s3g_init()
{
    gralloc_s3g_t *s3g = &HAL_MODULE_INFO_SYM;
    //all gralloc device share one s3g device
    fd = s3gOpenMinor(0);
    status = s3gCreateDdevice(fd, &create);
    //保存fd和device
    s3g->fd = fd;
    s3g->device = create.device;
}

//libkeinterface/s3g_keinterface.c
int s3gCreateDevice(int fd, s3g_create_device_t* create_device)
{
    if(ioctl(fd, S3G_IOCTL_CREATE_DEVICE, create_device))
    {
        return -errno;
    }
    return 0;
}

 

 

13,release方法

任务:

1,释放由open分配的,保存在flip->private_data的所有内容,

2,在最后一次操作时关闭设备

 

如果关闭device的次数比打开的多怎么办?

因为dup和fork系统调用都是不调用open,就创建以打开文件的副本。但是每个副本在程序终止时都要关闭。

如,多数程序从来不打开他们的stdin设备,但是都在终止时关闭,

driver如何知道何时真正关闭?

回答:

不是每个close系统调用都引起release的调用,真正释放设备数据结构的close才做。

内核对每个file结构维护其被使用次数的计数器。

fork和dup,都不会创建新的数据结构,只有open才会创建新的数据结构,fork和dup只是增加已有结构的计数。

只有file结构的计数归0时,close系统调用才会执行release方法,

这样就保证了一次open对应一次release

 

在进程退出时,内核在内部用close系统调用自动关闭所有相关文件,

 

14,scull的内存使用,

引入内核 内存管理的核心函数,在linux/slab.h

void *kmalloc(size_t size, int flags); void kree(void *ptr);

 

kmalloc分配size字节的内存,返回其指针,flags一般是GFP_KERNEL,

 

对于分配大内存区,kmalloc不是最好的方法,第八章讲,

分配整个页面更有效

 

为测试内存短缺,可以让scull吃光内存,

cp /dev/zero /dev/scull0,可以用光RAM

也可以用dd工具选择复制多少数据到scull设备

 

scull中,每个设备都是一个指针链表,每个指针指向一个scull_qset结构,

用来一个有1000个指针的数组,每个指针指向一个4000字节的区域。

 

。。。。介绍了scull_qset数据结构相关

 

14,read和write

他们的任务是copy data到UMD,反过来就是从UMD copy data。所以原型相似:

ssize_T read(struct file *filp, char __user *buff, size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buffer, size_t count, loff_t *offp);

 

filp是文件指针,count是请求传输的数据长度,buffer是UMD的缓冲区,

offp是指向long offset type对象的指针,它指明用户在文件中做存取操作的位置。

 

buff是UMD的指针,内核不能直接引用其中的内容,原因:

1,UMD的指针在KMD可能是无效的,该地址可能根本无法被映射到KMD,

2,即使指针在KMD代表相同的东西,但UMD的内存是分页的,而在系统调用被调用时,涉及的内存可能根本不在RAM中,

对UMD内存的直接饮用会导致页错误,oops会导致调用这个系统调用的进程死亡。

3,不安全,如果driver盲目引用UMD指针,UMD就可以随意访问和覆盖系统内存

 

这种访问由UMD专用函数完成,在asm/uaccess.h定义,六章ioctl介绍。

 

对于UMD和KMD的数据拷贝,使用:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long count); unsigned long copy_frome_user(void *to, const void __user *from, unsigned long count);

 

它的行为像memcpy,但是要小心,

被寻址的UMD的page可能不在内存,这是虚拟内存子系统会让进程进入休眠状态,直到page在期望的位置,

如page必须从swap空间取回时。

这样的结果是,访问UMD的函数必须是可重入的,必须能和其他driver函数并发执行,第五章讨论

 

他们得作用不限于copy data,还检查了UMD的指针是否有效,

无效UMD point在第六章讨论。

 

下面介绍了read和write的代码

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Linux设备驱动)