Linux设备驱动程序学习笔记

主题:Linux设备驱动程序
简介:第一次学习Linux设备驱动
参考:
Linux设备驱动程序第三版
设备驱动程序简介
构造和运行模块

作者:ybb
时间:2022年4月27日

第一章:设备驱动程序简介

1.1综述

设备驱动程序是进入linux内核世界的大门;
设备驱动程序是一个个独立的黑盒子,使某个特定硬件响应一个定义良好的内部编程接口,这些接口隐藏了设备的工作细节;
用户的操作通过一组标准化的调用执行,而这些调用独立于特定的驱动程序;
驱动程序的任务是将标准化的调用映射到设计硬件设备;
驱动程序独立于内核的其他部分而建立???
学习编写linux设备驱动程序,需要经常和内核打交道;

1.2驱动程序的作用

设备驱动程序提供的是机制,应用程序提供的是策略:
机制:需要提供什么功能(离不开特定硬件的支持);
策略:如何使用机制提供的功能;

注:编写访问硬件的内核代码时,只需要提供完善的机制即可,不要给用户强加任何特定的策略;

设备驱动程序可以看作应用程序和实际设备之间的一个软件层,驱动程序的设计需要考虑3个因素:
机制的完善性;
开发的时间和成本;
简单易用;

常见的驱动:打印机驱动、cd驱动、网卡驱动…

因为要编写驱动程序,所以就要了解内核;

1.3内核功能划分

5个功能:
进程管理:
内存管理:
文件系统:
网络功能:
设备控制:所有设备控制操作都由控制设备相关的代码来完成,这段代码就称为驱动程序。

1.4模块的装载和移除

insmod
rmmod

dmesg

1.5设备和模块的分类

块设备:
字符设备:
网络接口:

第二章:构造和运行模块

2.1概述

模块编程
内核编程

2.2设置测试系统

2.3 Hello World模块

#include 
#include 
#include 

static
int __init hello_init(void)
{
	printk(KERN_ALERT "Hello, world!\n");
	return 0;
}

static
void __exit hello_exit(void)
{
	printk(KERN_ALERT "Bye, World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");


2.3.1 Makefile

# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
    hello_world-objs := main.o
    obj-m := hello_world.o

# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)

.PHONY: modules
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

.PHONY: clean
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

2.4内核模块与应用程序的对比

大多数中小规模的应用程序是从头到尾执行单个任务。模块只是预先注册自己以便服务于将来的某个请求,然后其初始化函数立即结束,模块的退出函数将在模块被卸载之前调用。内核模块的编程方式和事件驱动的编程有些类似。
应用程序在退出时可以不管资源的释放或其他的清除工作。但模块的退出函数必须仔细撤销初始化函数所作的一切,否则,在系统重新引导之前某些东西就会残留在系统中。
应用程序可以调用它并未定义的函数,因为连接过程能够解析外部引用从而使用适当的函数库。但模块仅被链接到内核,它能调用的函数仅仅是由内核导出的那些函数,而不存在任何可链接的函数库。
模块源文件中不能包含通常的头文件,只能使用作为内核一部分的函数。
应用程序开发过程中的段错误是无害的,并且总是可以使用调试器跟踪到源代码中的问题所在;内核错误即使不影响整个系统,也至少会杀死当前进程。

2.4.1用户空间和内核空间

模块运行在内核空间
应用程序运行在用户空间

操作系统:
操作系统为应用程序提供一个对计算机硬件的一致视图;
操作系统负责程序的独立操作并保护资源不受非法访问;

人们在CPU中实现不同的操作模式(或者级别)。不同的级别具有不同的功能,在较低的级别中将禁止某些操作。程序代码只能通过有限数目的“门” 从一个级别切换到另一级别。Unix使用了两个这样的级别:内核运行在最高级别(也称作超级用户态),可以进行所有操作;应用程序运行在最低级别(即所谓的用户态),处理器控制着对硬件的直接访问以及对内存的非授权访问。两种模式具有不同的优先权等级,每个模式都有自己的内存映射,即自己的地址空间。

执行系统调用的内核代码在进程上下文中,它代表用户进程执行操作,可以访问进程地址空间的所有数据;
处理硬件终端的内核代码和进程是异步的,与任何一个特定进程无关;

一个驱动程序需要执行的两类任务:
某些函数作为系统调用的一部分执行;
其他函数负责中断处理;

Linux系统上的/proc目录是一种文件系统,它是一种虚拟文件系统,存储的是当前内核运行状态的特殊文件,用户可以通过/proc内的文件查看有关系统硬件以及当前正在运行进程的信息,也可以通过改变某些文件来改变内核的运行状态。

2.4.2内核中的并发

内核编程注重于对并发的处理!!!
内核编程区别于常见应用程序编程的地方在于对并发的处理。即使是最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生。

内核编程必须考虑并发的原因

Linux系统中通常正在运行多个并发进程,他们可能同时使用我们的驱动程序。
大多数设备能够中断处理器,而中断处理程序异步运行,而且可能在驱动程序试图处理其他任务时被调用。
一些抽象软件,例如内核定时器,也在异步运行。
Linux可以运行在SMP(同步多处理器 )系统上,可能同时有不止一个CPU运行我们的驱动程序。
在2.6中内核代码已经是可抢占的,意味着即使在单处理器系统上也存在许多类似多处理器系统的并发问题。

linux内核代码(包括驱动程序代码)必须是可重入的,它必须能够同时运行在多个上下文中,对编写正确的内核代码来说,需要优良的并发管理。

2.4.3当前进程

虽然内核模块不想语言程序那样顺序执行,然而内核之心给的大多数操作还是和某个特定的进程相关;

内核代码可通过访问全局项current来获得当前进程。它是一个指向struct task_struct的指针。current指针指向当前正在运行的进程。内核代码可以通过current获得与当前进程相关的信息。例如,下面的语句通过访问struct task_struct的某些成员来打印当前进程的ID和命令名:

printk(KERN_INFO "The process is \" %s \" (pid %i)\n",current->common,current->pid);

为了支持SMP系统,内核开发这设计了一种能找到运行在相关CPU上的当前进程的机制。它必须是快速的,因为对current的引用会很频繁。一种不依赖于特定架构的机制通常是,将指向task_struct结构的指针隐藏在内核栈中。

2.4.4其他细节

深入内核研究的同时,应该时刻牢记以下问题:
栈是用来保存函数调用历史以及当前活动函数中的自动变量;
应用程序在虚拟内存中布局,具有一块很大的栈空间;
内核具有非常小的栈,可能只有一个4096字节的页那么大;
我们自己的函数(哪里的)必须和整个内核空间调用链一同共享这个栈;
声明大的自动变量会占据更多的栈地址;
如果需要大的结构,应该在调用时动态分配结构,因为内核的栈很小;
具有__的函数,仅用于模块初始化或者清除阶段通常是接口的底层组件,谨慎使用;
内核代码不能实现浮点数运算;

2.5编译和装载

2.5.1编译模块

从目标文件hello.o构造模块hello.ko
obj-m :=hello.o
hello.o来自于hello.c
注:C语言学习笔记之gcc编译器
Makefile文件想要正常工作,必须在大的内核构造系统环境中调用他们。
改变目录到内核源代码目录,该目录保存内核的顶层makefile文件;
M=选项让makefile在构造模块modules目标之前返回到模块源代码目录;
modules目标指向obj-m变量中设定的模块;
交叉编译???
make -C /usr/src/linux-headers-x.x.x-generic M=‘pwd’ modules
Makefile示例程序:
#如果已经定义了KERNELRELEASE,我们就被kernel build系统调用,并且能够使用它。

ifneq($(KERNELRELEASE),)
	obj-m :=hello.o

#注:https://blog.csdn.net/liukun321/article/details/6874489
#注:如果是多个.c文件 ???

#如果没有定义KERNELRELEASE,我们将从命令调用kernel build系统
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD :=$(shell pwd)

.PHONY: modules
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

.PHONY: clean
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

注:Linux软链接与硬链接

2.5.2装载和卸载模块

先完成make -n

insmod将模块的代码和数据装入内核,然后使用内核的符号表解析(公共内核符号表包含了所有全局内核项也即函数和变量的地址)。
内核不会修改模块的磁盘文件,仅仅修改内存中的副本,因此与链接器不同。
insmod可以接收命令行选项,可以在模块链接到内核之前给模块的整型和字符串变量赋值。
模块可以在装载时进行配置。

sys_init_module函数给模块分配内核内存,栈内存???
有且只有系统调用的名字前带有sys_前缀。

modprobe将模块装载到内核中,比insmod更先进,因为modprobe会考虑要装载的模块是否引用了一些当前内核不存在的符号。

rmmod将模块从内核中移除。

其他辅助指令:lsmod dmesg

2.5.3版本依赖

模块和内核需要匹配。
在build过程中,可以将模块和vermagic.o链接,该目标文件包含许多有关内核的信息,当试图装载模块时,这些信息可以用来检查模块和内核的兼容性。

使用宏以及#ifdef来编译模块代码,可以使模块工作在不同的内核。
注:依赖于特定版本的代码应该隐藏在底层宏或者函数之中。
UTS_RELEASE,该宏扩展为一个描述内核版本的字符串
LINUX_VERSION_CODE,该宏扩展为为内核版本的二进制表示,版本发行号中的每一部分对应一个字节
KERNEL_VERSION(major,minor,release),该宏以组成版本号的三个部分,创建整数的版本号

2.5.4平台依赖

连接vermagic.o

2.6内核符号表(还没理解)

insmod使用公共内核符号表来解析模块中未定义的符号。公共内核符号表包含了所有的全局内核项(函数和变量)的地址,它是实现模块化驱动程序所必须的。

模块装入内核之后,它所导出的任何符号都会成为内核符号表的一部分。
模块层叠技术:一般来说,模块只需实现自己的功能,无需导出任何符号,如果模块之间互相依赖,则需要导出符号,这样就可以在其他模块上层叠(多个模块依赖)新的模块。
注:模块不许在模块文件的全局部分导出,不能在函数中导出。

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

2.7预备知识

内核一个特殊的环境,代码编写由一定的要求:
#include
#include
#include
#include

MODULE_LICENSE(“GPL”)

2.8初始化和关闭

模块的初始化函数负责注册模块所提供的任何设施
初始化函数被声明为static,它在特定文件之外没有其他意义。但这并不是一个强制性规则,因为一个模块函数如果要对内核其他部分可见,则必须被显式导出。
__init标记表明该函数仅在初始化期间使用。载模块被装载后,模块装载器会将初始化函数丢弃,以释放其占用的内存。__init和 __initdata很值得使用,但请注意,不要在结束初始化之后仍要使用的函数或数据上使用它们。 module_init()的使用是强制的。这个宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。

static int hello_init(void){
printf(KERN_ALERT “hello,world\n”);
return 0;
}
module_init(hello_init);

清除函数: 每个重要的模块都需要一个清除函数,清除函数负责在模块被移除前注销接口并向系统返回所有资源。 清除函数没有返回值;
module_exit()帮助内核找到模块的清除函数;
如果一个模块未定义清除函数,则内核不允许卸载该模块;

static hello_exit(void){
printk(KERN_ALERT “Bye,world”);
}
module_exit(hello_exit);

MODULE_AUTHOR(“YBB”)
MODULE_DESCRIPTION(“TEST”)
MODULE_VERSION(“V1”)
MODULE_ALIAS(“TESTALIAS”)
MODULE_DEVICE_TABLE(“???”)

初始化过程的错误处理:
内核注册设施时可能会失败,因此模块代码必须始终检查返回值,确保所有的操作已真正成功。

如果注册设施时遇到错误,首先判断模块是否可以继续初始化,通常在某个注册失败后可以通过降低功能来继续运转;

如果在发生了某个特定类型的错误之后无法继续装载模块,则必须将之前注册的工作撤销;

使用goto语句进行错误恢复的处理(之撤销出错时刻以前所成功注册的设施),可以避免大量复杂的、高度缩进的结构化逻辑;

int  my_init_func(void)
{
	int err;
	/*Register using pointers and names */
	err = register_this(ptr1,"skull");
	if(err)
		goto fail_this;
	err = register_that(ptr2,"skull");
	if(err)
		goto fail_that;
	err = register_those(ptr3,"skull");
	if(err)
		goto fail_those;
	/*Success*/
	return 0;

fail_those:
	unregister_that(ptr2,"skull");
fail_that:
	unregister_this(ptr1,"skull");
fail_this:
	return err;
}

模块的清除函数需要撤销初始化函数所注册的所有设施,以相反于注册的顺序撤销注册的设施;

void my_cleanup_function(void)
{
	unregister_those(ptr3,"skull");
	unregister_that(ptr2,"skull");
	unregister_this(ptr1,"skull");
	return
}

每当发生错误时从初始化函数调用清除函数,将减少代码的重复,使得代码更清晰有条理;
清除函数在撤销设施的注册之前需要检查它的状态;

清除函数被非退出的代码调用,不能将清除函数标记为__exit

struct something *item1;
struct somethingelse *item2;
int stuff_ok;

void my_cleanup(void)
{
	if(item1)
		release_thing(item1);
	if(item2)
		release_thing2(item2);
	if(stuff_ok)
		unregister_stuff();
	return;
}

int __init my_init(void)
{
	int err = -ENOMEM;
	item1 = allocate_thing(arguments);
	item2 = allocate_thing2(arguments2);
	if(!item1 || !item2)
		goto fail;
	err = register_stuff(item1,item2);
	if(!err)
		stuff_ok = 1;
	else
		stuff_ok = 0;
	return 0;

fail:
	my_cleanup();
	return err;
}

模块装载竞争:
注册完成之后,内核的某些部分可能会立即使用我们刚刚注册的任何设施,也就是说,初始化函数还在运行的时候,内核就有可能调用我们的模块,此时初始化还未结束,因此在首次注册完成后,代码就应该准备好被内核的其他部分调用;
在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施???

2.9模块参数

模块参数可在运行insmod或者modprobe命令时赋值,而modprobe还可以从配置文件(/etc/modprobe.conf)中读取参数值???

在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见,参数通过module_param宏来声明。

  1. 变量的名称
  2. 类型
  3. 用于sysfs入口项的访问许可掩码

注:基于内存的虚拟化文件系统sysfs
模块的装载器也支持数组参数,在提供数组值时用逗号划分各数组成员。要声明数组参数,使用宏module_param_array(name,type,num,perm)。模块装载器会拒绝接受超过数组大小的值;

如果我们需要的类型不在上面列出的清单中,可以使用模块代码中的钩子来定义这些类型。

所有模块参数都应给定一个默认值,insmod只会在用户明确设置了参数的值的情况下参会改变参数的值。

module_param的最后一个参数是访问许可值,它用来控制谁能访问sysfs中对模块参数的表述:

如果perm被设置为0,就不会有对应的sysfs入口项;
否则模块参数会在sys/module中出现,并设置为给定的访问许可;

如果一个参数通过sysfs被修改,内核不会以任何方式通知模块,除非特殊需要,不应该让模块参数可写。

#include 
#include 
#include 

static char *whom = "Mom";
static int howmany = 1;

module_param(howmany, int,   S_IRUGO);
module_param(whom,    charp, S_IRUGO);

static
int __init m_init(void)
{
	printk(KERN_WARNING "parameters test module is loaded\n");

	for (int i = 0; i < howmany; ++i) {
		printk(KERN_WARNING "#%d Hello, %s\n", i, whom);
	}
	return 0;
}

static
void __exit m_exit(void)
{
	printk(KERN_WARNING "parameters test module is unloaded\n");
}

module_init(m_init);
module_exit(m_exit);

MODULE_LICENSE("GPL");


注:内核文件权限值
第三章 字符设备驱动程序
3.1 scull的设计
scull:simple character utility for loading localities区域装载的简单字符工具
为什么首先学习是字符设备驱动程序呢,因为字符驱动程序简单容易理解(向较于网络设备和块设备而言),而且字符设备驱动程序适合大多数硬件设备。
前面提到过内核编程关注的是机制,因此编写字符设备驱动程序首先需要定哟驱动程序能够提供的机制。
scull的源代码实现了以下设备,我们将由模块实现的某种设备称作一种类型???
scull0~scull3
scullpipe0~scullpipe3:FIFO,阻塞式和非阻塞式读写???
scullsingle
sculluid
scullwuid

3.2主设备major和次设备minor
对字符设备的访问是通过文件系统内的设备名称进行的,这些名称被称为特殊文件、设备文件、文件系统树的节点
/dev
linux dev目录
c:字符设备
b:块设备

主设备号:标识设备对应的驱动程序(现在linux内核允许多个驱动设备共享主设备号)
次设备号:由内核使用,用于正确确定设备文件所指的设备,可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当做设备本地数组的索引。

如何获得dev_t的主设备号和次设备号:

MAJOR(dev_t dev);
MINOR(dev_t dev);

如何将主设备号和次设备号转换成dev_t类型:

MKDEV(int major,int minor);

设备编号的分配和释放:
在建立一个字符设备之前,驱动程序首先要做的就是获得设备编号;
如果设备编号编号已知,可以使用

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

注:register_chrdev_region()函数详解

如果不知道设备使用那些主设备号,可以通过

int alloc_chardev_region(dev_t *dev,unsigned int first major,unsigned int count,char *name)

让内核分配所需要的主设备号

注:alloc_chrdev_region()函数详解

设备编号使用完应该释放,可以通过

void unregister_chrdev_region(dev_t first,unsigned int count);

释放设备编号

注:unregister_chrdev_region

注:一般unregister_chrdev_region在模块的清除函数中调用

设备编号的注册、分配,没有告诉内核用这些编号做什么工作,在用户空间程序可以访问上述设备编号之前,驱动程序需要将设备编号和内部函数连接起来。

动态分配主设备号:
驱动程序应该始终使用alloc_chrdev_region而不是register_chrdev_region函数。
然而,动态分配的主设备号可能不一致,所以无法预先创建设备节点。
因此,为了加载一个使用动态主设备号的驱动程序,对insmod的调用可替换为一个简单的脚本。
该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件。
动态分配主设备号的情况下,要加载这类驱动程序模块的脚本,可以利用awk这类工具;
从/proc/devices中获取信息,并在/dev目录中创建设备文件。

#!/bin/bash
module="scull"
device="scull"
mode="666"
group=0

function load() {
    insmod ./$module.ko $* || exit 1 #在||左侧语句的返回值$?为1,即执行失败时,才会执行||右侧的语句

    rm -f /dev/${device}[0-2]

    major=$(awk -v device="$device" '$2==device {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

    chgrp $group /dev/$device[0-2]
    chmod $mode /dev/$device[0-2]
}

function unload() {
    rm -f /dev/${device}[0-2]
    rmmod $module || exit 1
}

arg=${1:-"load"}
case $arg in
    load)
        load ;;
    unload)
        unload ;;
    reload)
        ( unload )
        load
        ;;
    *)
        echo "Usage: $0 {load | unload | reload}"
        echo "Default is load"
        exit 1
        ;;
esac

注:脚本中load函数的最后几行改变了设备的组和访问模式,这是因为该脚本必须由超级用户运行,创建的设备文件属于root。默认的权限位只允许root对其有写访问权而其他用户只有读权限。

主设备号的最佳分配方式:默认采用动态分配,同时保留在加载甚至是编译时指定主设备的余地???

if(scull_major)
{
	dev = MKDEV(scull_major,scull_minor);
	result = register_chrdev_region(dev,scull_nr_devs,"scull");
}
else
{
	result = alloc_chrdev_region(&dev,scull_minor,scull_nr_devs,"scull");
	scull_major = MAJOR(dev);
}
if(result < 0)
{
	printk(KERN_WARNING "scull:can't get major %d\n",scull_major);
	return result;
}

3.3重要的数据结构
file
file_operations
inode
3.3.1 file结构(表示文件描述符)
file结构与用户空间程序的FILE没有任何关联;
file结构代表一个打开的文件,系统中每个打开的文件在内核空间都有一个对应的file结构,它由内核在open时创建,并传递给在该文件上进行操作的所有函数,在文件的所有实例都被关闭后,内核会释放这个数据结构。
在内核源代码中,指向struct file的指针称为file。
3.3.2 inode结构(表示文件)
内核使用inode结构在内部表示文件,它统一file结构不同,file结构表示打开的文件描述符,对于单个文件,可能会有许多个表示打开的文件描述符的file结构,但他们都指向单个inode结构。
inode结构中只有以下两个字段对编写驱动程序代码有用:
dev_t i_rdev;
struct cdev *i_cdev;
可以通过下面的宏,从inode中获得主设备号和次设备号:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

3.3.3 file_operations
file_operations结构用来建立设备编号和驱动程序操作之间的连接。
这个结构中包含一组函数指针(指向函数的指针),每个打开的文件由一个file结构表示和一组函数关联(通过file结构中包含指向一个file_operations结构的字段f_op)
这些操作主要用来实现系统调用,例如open、read
因此可以认为:文件是一个对象,而方法是操作它的函数。

file_operations结构或指向这类结构的指针称为fops;
该结构中的每个字段必须指向驱动程序中实现特定操作的函数,对于不支持的字段可以设置为NULL值。
对于各个函数而言,如果对应的字段被赋值为NULL指针,那么内核的具体行为是不同的。
注:file_operations结构的方法指针中,有许多参数包含__user字符串,它是一种形式的文档而已,表明指针是一个用户空间地址,因此不能被直接引用。对通常的编译来讲,__user没有任何效果,但是可以由外部检查软件使用,用来寻找对用户空间地址的错误使用。

scull设备驱动程序的file_operations结构:

struct file_operations scull_fops = 
{
	.owner = THIS_MODULE,
	.llseek = scull_llseek,
	.read = scull_read,
	.write = scull_write,
	.ioctl = scull_ioctl,
	.open = scull_open,
	.release = scull_release,
};

3.4 字符设备的注册
内核内部使用struct cdev结构表示字符设备;
在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev结构。

struct cdev结构的分配和初始化方式:

在运行时获取一个独立的cdev结构:

struct cdev *my_cdev=cdev_alloc();
my_cdev->ops=&my_ops;

将cdev结构嵌套到自己设备的特定结构

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

此外struct cdev中也有一个所有者字段,应该被设置为THIS_MODULE

设置好cdev结构,通过下面的调用告诉内核该结构的信息
int cdev_add(struct cdev *cdev,dev_t num,unsigned int count);
注:cdev_add注册设备可能会失败;
只要cdev_add返回了,设备就激活了,对设备的操作会被内核调用;
如果驱动程序还没有完全准备好处理设备上的操作,就不能调用cdev_add

字符设备的移除:
void cdev_del(struct cdev *dev);
将cdev结构传递到cdev_del函数后,就不应该再访问cdev结构了

scull中的设备注册:
在scull内部,通过struct scull_dev结构表示每个设备

struct scull_dev
{
	struct scull_qset *data;
	int quantum;
	int qset;
	unsigned long size;
	unsigned int access_key;
	struct semaphore sem;
	struct cdev cdev;
}

struct scull_dev中的cdev字段,必须初始化并添加到系统中:

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_MODULE;
	dev->cdev.ops = &scull_fops;
	err = cdev_add(&dev->cdev,devno,1);
	if(err)
		printk(KERN_NOTICE "Error %d adding scull%d",err,index);
}

将主设备号和次设备号转换成dev_t类型:

MKDEV(int major,int minor);

获得主设备号和次设备号:

MAJOR(dev_t dev);
MINOR(dev_t dev);

3.5 open和release
open提供给驱动程序初始化的能力,从而为以后的操作完成初始化做准备。
在大部分驱动程序中,open应完成如下工作:
检查设备特定的错误
如果是首次打开,则对其进行初始化
如有必要,更新f_op指针
分配并填写置于filp->private_data里的数据结构

首先需要确定要打开的具体设备;

open方法的原型:

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

注:内核用inode结构在内部表示文件,file结构表示打开的文件描述符

inode结构对于边学驱动程序代码有用的两个字段
dev_t i_rdev;
struct cdev *i_cdev;

inode参数在其i_cdev字段中包含了确定要打开的具体设备所需要的信息;
唯一的问题是,通常不需要cdev结构本身(内核内部使用struct cdev结构表示字符设备),而是希望得到包含cdev结构的结构,如scull_dev结构,通过container_of宏可以完成这种转换,这个宏需要一个container_field字段的指针pointer,它包含在container_type类型的结构中。

container_of(pointer,container_type,container_field);

一旦找到了scull_dev结构后,scull将其保存到了file结构的private_data字段

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

release:
release的作用域open相反,release完成:
释放由open分配的、保存在filp->private_data中的所有内容
在最后一次关闭操作时关闭设备;

注:只有那些真正释放设备数据结构的close调用才会调用release。内核对每个file结构维护其被使用多少次的计数器,无论是fork还是dup都不会创建新的数据结构(新的数据结构仅有open创建,open可以提供给程序初始化的能力,从而为以后的操作完成初始化做准备),它们指示增加了已有结构中的计数。只有在file结构的计数归0时,close系统调用采用调用release方法(无open不是release),这只在删除这个结构时才会发生。release方法和close系统调用的关系保证了对于每次open驱动程序只会看到对应的一次release调用。

read和write:
read:读出
write:写入

read和write的原型:

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

read和write方法的buff是用户空间的指针,内核代码不能直接引用其中的内容,原因如下:
在内核模式中运行时,用户空间的指针可能是无效的;
即使指针有效,用户空间的内存是分页的,在系统调用被调用时,涉及到的内存可能不在RAM中,对用户空间内存的直接引用将导入错误,这对内核代码来说是不允许发生的;
如果驱动程序盲目引用用户空间提供的指针,将导致系统出现打开的后门,允许用户空间程序随意访问或覆盖系统中的内存。

你可能感兴趣的:(linux,linux,驱动开发)