文章目录
- 第八章、设备管理
- 8.1、设备管理概念
- 8.1.1、设备分类
- 8.1.2、设备管理的主要功能
- 8.2、Spooling系统——虚拟技术
- 8.2.1、什么是独占型设备和共享型设备
- 8.2.2、独占型设备的分配和共享型设备的分配、
- 8.2.3、虚拟分配
- 8.3、Linux模块机制
- 8.3.1、Linux驱动程序(LDD)
- 8.3.2、Linux的驱动程序
- 8.3.2.1、LDD程序结构(Linux device driver,Linux设备驱动程序)
- 8.3.2.2、简单的字符驱动程序——实现5个函数
- 8.3.2.3、LDD程序的加载方式
第八章、设备管理
8.1、设备管理概念
所谓的设备,就是我们所说的外设,比如鼠标、键盘等,除了cpu、内存之外的设备。如图所示:
外设众多,那么我们如何来管理外设呢?
8.1.1、设备分类
我们要管理众多的外设,首先是将众多的设备进行分类。分类如图所示:
下面我们来了解一些常见的外设内部结构:
1.鼠标
2.键盘
3.打印机
4.硬盘
一个思考题:操作系统公司会不会去专门花费精力去编写外设的控制程序/驱动程序呢?
答案是不会,操作系统提供统一的接口,只要驱动开发厂商实现对应的接口函数实现,由操作系统提供相应的API注册实现。
8.1.2、设备管理的主要功能
- 设备分配——最基本的功能
设备分配功能是设备管理的基本任务,设备分配程序按照一定的策略,为申请设备的进程分配设备,记录设备的使用情况。 - 设备映射
- 这里还要提到一个概念:设备的独立性——物理设备对用户透明,用户使用统一规范的方式使用设备。同时用户编程时使用设备逻辑名,由系统实现逻辑设备到物理设备的转换。
友好名
- 设备驱动——对物理设备进行控制,实现I/O操作:IN/OUT;接受应用程序的服务请求(例如读/写命令),转换为具体的I/O指令,控制设备完成相关的操作。向用户提供统一的接口(read/write/open…把外设作为特别的文件处理)
对应的设备驱动程序——操作系统仅对设备驱动的接口提出要求。
8.2、Spooling系统——虚拟技术
我们知道设备分配的方法有
- 独占设备的分配
- 共享设备的分配
- 虚拟分配
8.2.1、什么是独占型设备和共享型设备
独占型设备包括所有的字符型设备。使用完毕,对应的进程必须释放设备。可能引起进程的阻塞。
共享型设备包括所有的块设备。I/O传输单位是块。无需申请或释放设备。
8.2.2、独占型设备的分配和共享型设备的分配、
独占型设备
对应的进程必须先申请,申请不到就会进入阻塞。
共享型设备
8.2.3、虚拟分配
虚拟技术
首先我们来了解一下虚拟技术——在一类物理设备上模拟另一类物理设备的技术(原理:利用辅存部分区域模拟独占设备,将独占设备转化为共享设备)
虚拟设备
用来模拟独占设备的部分辅存称为虚拟设备,用来虚拟独占设备
- 输入井:模拟输入设备的辅存区域
- 输出井:模拟输出设备的辅存区域
Spooling
虚拟分配中的虚拟技术有很多,常用于windows的有Spooling系统,它是虚拟技术和虚拟分配的实现。
我们来看一下Spooling到底是如何进行虚拟分配的
具体的过程如图:
spooling系统原理小结
8.3、Linux模块机制
8.3.1、Linux驱动程序(LDD)
Linux模块的概念
Linux设备驱动会以内核模块的形式出现,因此,学会编写Linux内核模块编程是学习Linux设备驱动的先决条件。
Linux内核模块
- Loadable kernel MOdule:LKM
- 解决了单体内核机制的不足
- 一种未经链接的可执行代码
- 经过装载(即链接)成为内核的一部分
- 可以动态加载或者卸载
Linux的内核模块机制允许开发者动态的向内核添加功能,我们常见的文件系统、驱动程序等都可以通过模块的方式添加到内核而无需对内核重新编译,这在很大程度上减少了操作的复杂度。模块机制使内核预编译时不必包含很多无关功能,把内核做到最精简,后期可以根据需要进行添加。而针对驱动程序,因为涉及到具体的硬件,很难通用的,且其中可能包含了各个厂商的私密接口,厂商几乎不会允许开发者把源代码公开,这就和linux的许可相悖,模块机制很好的解决了这个冲突,允许驱动程序后期进行添加而不合并到内核。OK,下面结合源代码讨论下模块机制的实现。
类似于普通的可执行文件,模块经过编译后得到.ko文件,其本身也是可重定位目标文件,类似于gcc -c 得到的.o目标文件。对于可重定位的概念,请参考PE文件格式(虽然是windows下的,但是原理类似)。
重定位
既然是重定位文件,在把模块加载到内核的时候就需要进行重定位,回想下用户可执行文件的重定位,一般如果一个程序的可执行文件总能加载到自己的理想位置,所以对于用户可执行文件,一般不怎么需要重定位,而对于动态库文件就不同了,库文件格式是一致的,但是可能需要加载多个库文件,那么有些库文件必然无法加载到自己的理想位置,就需要进行重定位。而内核模块由于和内核共享同一个内核地址空间,更不能保证自己的理想地址不被占用,所以一般情况内核模块也需要进行重定位。在加载到内核时,还有一个重要的工作即使解决模块之间的依赖,模块A中引用了其他模块的函数,那么在加载到内核之前其实模块A并不知道所引用的函数地址,因此只能做一个标记,在加载到内核的时候在根据符号表解决引用问题!这些都是在加载内核的核心系统调用sys_init_module完成。
每一个内核模块在内核中都对应一个数据结构module,所有的模块通过一个链表维护。所以有些恶意模块企图通过从链表摘除结构来达到隐藏模块的目的。
可以参考博客
制作简单的Linux驱动程序
第一步,编写驱动模块程序
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void){
printk(KERN_ALERT" hello锛宨nstall kernel.\n");
return 0;
}
static void __exit hello_exit(void){
printk(KERN_EMERG" removed kernel.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
最后一句是对Dual BSD/GPL许可权限的声明。内核模块中用于输出的函数是内核空间的printk()而非用户空间的printf(),printk()的用法和printf()基本相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段
第二步,编译模块
编写makefile文件:
ifneq ($(KERNELRELEASE),)
MODULE_NAME = hellomodule
$(MODULE_NAME)-objs := hello.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)
.PHONY: modules
default: modules
modules:
make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
clean distclean:
rm -f *.o *.mod.c .*.*.cmd *.ko
rm -rf .tmp_versions
endif
在编译过程中提示没有linux\init.h头文件,说明我们我们缺少相关的头文件,故安装头文件:
sudo apt-get install linux-headers-$(uname -r)
然后安装linux 内核的源代码:
sudo apt-get install linux-source
重新编译hello.c文件
make
第三步,安装模块
sudo insmod hellomodule
sudo remod hellomodule
安装了模块,如果打印日志的级别不够Linux的控制台级别,则需要添加dmesg来查看输出,同样,移除驱动模块的时候,也是要这样做。
dmesg
我们也可以列出安装的驱动:
lsmod
可以看到hellomodule这个模块。
8.3.2、Linux的驱动程序
我们已经做完了一个简单的linux驱动模块,这次我们来详细了解一下linux驱动程序。
8.3.2.1、LDD程序结构(Linux device driver,Linux设备驱动程序)
Linux有两种运转模式——内核态和用户态。而驱动程序工作在内核态。应用程序通过驱动程序间接访问设备
内核态具有较高的权限,可以控制处理器内存的映射和分配方式 ,访问外设空间和处理器状态寄存器,控制中断等。通过get_user put_user copy_from_user copy_to_user等函数实现应用程序和驱动程序之间传送数据(指针)。
所以我们要了解LDD程序,必须先知道Linux的设备分类。
Linux的设备分类
- 字符设备
以字节为单位逐个进行I/O操作——如串口设备 - 块设备
块设备的存取是通过buffer、cache进行的。它可以进行随机访问——如IDE硬盘设备、支持可安装文件系统 - 网络设备
通过BSD套接口访问(SOCKET,套接字)
有设备结点不一定对应物理设备。比如伪设备:
设备文件
Linux里面我们必须明确什么是设备文件,Linux抽象了对硬件的处理,所有的硬件设备都可以作为普通文件看待。字符设备和块设备是通过文件节点来访问的。
- 把硬件设备当作文件来看待,就可以得到设备文件
- 对于设备的操作就相当于文件的操作,用文件接口完成对应设备的操作,如打开、关闭、读写、I/O控制等
- 字符设备和块设备通过设备文件访问。比如Linux文件系统中可以找到设备文件。
本质上讲,设备节点对应于操作系统分配的资源。Unix通过存放于节点结构中的主设备号和从设备号来识别这些资源。在各种操作系统和系统平台上,这些数都是被唯一分配的。通常,主数用于指定驱动程序,而次数用于指定驱动程序控制的某一特定设备(驱动程序可能控制多个设备),在这种情况下,系统可能把次数作为参数传给驱动程序。
计算机就像对待普通文件那样,用标准系统调用访问设备节点。根据硬件的接口类型和操作系统处理输入输出的方式,设备文件可以分成两类。
我们可以使用ls -l /dev
的命令来列出linux相关的设备文件:
下面我们来了解一下什么是主设备号和从设备号:
- 主设备号——Linux支持动态分配主设备号。
- 从设备号(次设备号)
同一个驱动程序可以管理多个设备,他们依靠次设备号来区别。次设备号只在驱动程序内部使用,系统内核直接把次设备号传递给驱动程序,由驱动程序来管理。
Linux内核里面设备号使用dev_t来描述:
typedef u_long dev_t;
//在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
//可以使用下列宏从dev_t中获得主次设备号
MAJOR(dev_t dev);
MINOR(dev_t dev);
//也可以使用下列宏通过主次设备号生成dev_t:
MKDEV(int major,int minor);
基于上面的了解,我们可以得到完整的LDD结构
下面我们来做一个实战例子——简单的字符驱动程序,来了解如何写一个驱动及其相关的数据结构。
8.3.2.2、简单的字符驱动程序——实现5个函数
前备知识
如何分配设备号:
(1)静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
(2)动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
(3)注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count);
(4)创建设备文件
(5)相关数据结构
- struct file:代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
//重要成员:
const struct file_operations *f_op; //该操作是定义文件关联的操作的。内核在执行open时对这个指针赋值。
off_t f_pos; //该文件读写位置。
void *private_data;//该成员是系统调用时保存状态信息非常有用的资源。
- struct inode:用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 … … )被格式化为文件系统后,应该有两部份,一部份inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
dev_t i_rdev; //对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev; //是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
//我们也可以使用下边两个宏从inode中获得主设备号和此设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
- struct file_operations——文件操作结构体
文件结构体的初始化:
- 驱动程序、应用程序的数据交换
驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
put_user(local,user);
get_user(local,user);
了解了前备知识了。我们现在开始着手写一个简单的字符驱动程序。
编写cdevdemo.c文件:
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <asm/atomic.h>
#include <linux/slab.h>
#include <linux/device.h>
#define CDEVDEMO_MAJOR 255
/*预设cdevdemo的主设备号*/
static int cdevdemo_major = CDEVDEMO_MAJOR;
/*设备结构体,此结构体可以封装设备相关的一些信息等信号量等,
也可以封装在此结构中,后续的设备模块一般都应该封装一个这样的结构体,
但此结构体中必须包含某些成员,对于字符设备来说,我们必须包含struct cdev cdev
*/
struct cdevdemo_dev {
struct cdev cdev;
};
/*设备结构体指针*/
struct cdevdemo_dev *cdevdemo_devp;
/*文件打开函数,上层对此设备调用open时会执行*/
int cdevdemo_open(struct inode *inode, struct file *filp){
printk(KERN_NOTICE "======== cdevdemo_open ");
return 0;
}
/*文件释放,上层对此设备调用close时会执行*/
int cdevdemo_release(struct inode *inode, struct file *filp) {
printk(KERN_NOTICE "======== cdevdemo_release ");
return 0;
}
/*文件的读操作,上层对此设备调用read时会执行*/
static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) {
printk(KERN_NOTICE "======== cdevdemo_read ");
return 0;
}
/* 文件操作结构体,文中已经讲过这个结构*/
static const struct file_operations cdevdemo_fops = {
.owner = THIS_MODULE,
.open = cdevdemo_open,
.release = cdevdemo_release,
.read = cdevdemo_read,
};
/*初始化并注册cdev*/
static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index)
{
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1");
int err;
int devno = MKDEV(cdevdemo_major, index);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2");
/*初始化一个字符设备,设备所支持的操作在cdevdemo_fops中*/
cdev_init(&dev->cdev, &cdevdemo_fops);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3");
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &cdevdemo_fops;
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4");
err = cdev_add(&dev->cdev, devno, 1);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5");
if(err){
printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index);
}
return;
}
int cdevdemo_init(void) {
printk(KERN_NOTICE "======== cdevdemo_init ");
int ret;
dev_t devno = MKDEV(cdevdemo_major, 0);
struct class *cdevdemo_class; /*申请设备号,如果申请失败采用动态申请方式*/
if(cdevdemo_major){
printk(KERN_NOTICE "======== cdevdemo_init 1");
ret = register_chrdev_region(devno, 1, "cdevdemo");
}else{
printk(KERN_NOTICE "======== cdevdemo_init 2");
ret = alloc_chrdev_region(&devno,0,1,"cdevdemo");
cdevdemo_major = MAJOR(devno);
}
if(ret < 0){
printk(KERN_NOTICE "======== cdevdemo_init 3");
return ret;
}
/*动态申请设备结构体内存*/
cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL);
if(!cdevdemo_devp) /*申请失败*/
{
ret = -ENOMEM;
printk(KERN_NOTICE "Error add cdevdemo");
goto fail_malloc;
}
memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev));
printk(KERN_NOTICE "======== cdevdemo_init 3");
cdevdemo_setup_cdev(cdevdemo_devp, 0);
/*下面两行是创建了一个总线类型,会在/sys/class下生成cdevdemo目录
这里的还有一个主要作用是执行device_create后会在/dev/下自动生成
cdevdemo设备节点。而如果不调用此函数,如果想通过设备节点访问设备
需要手动mknod来创建设备节点后再访问。*/
cdevdemo_class = class_create(THIS_MODULE, "cdevdemo");
device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo");
printk(KERN_NOTICE "======== cdevdemo_init 4");
return 0;
fail_malloc:
unregister_chrdev_region(devno,1);
return 0;
}
void cdevdemo_exit(void) /*模块卸载*/
{
printk(KERN_NOTICE "End cdevdemo");
cdev_del(&cdevdemo_devp->cdev);
/*注销cdev*/
kfree(cdevdemo_devp);
/*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(cdevdemo_major,0),1); //释放设备号
}
module_param(cdevdemo_major, int, S_IRUGO);
module_init(cdevdemo_init); //注册
module_exit(cdevdemo_exit);//卸载
MODULE_LICENSE("Dual BSD/GPL");
编写makefile文件:
ifneq ($(KERNELRELEASE),)
obj-m := cdevdemo.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
执行make:
生成对应的模块文件:
因为我的笔记软件是windows里兼容比较好。所以我把它放到一个盘,让windows也能读取:
接下来我们安装模块:
查看:
卸载:
我们还可以添加测试函数:
1)cat /proc/devices看看有哪些编号已经被使用,我们选一个没有使用的XXX。
2)insmod memdev.ko
3)通过"mknod /dev/memdev0 c XXX 0"命令创建"/dev/memdev0"设备节点。
4)交叉编译app-mem.c文件,下载并执行:
#./app-mem,显示:
Mem is char dev!
预期结果:
实际过程:
必须创建节点,我们才能在dev文件夹里面看到对应的设备文件,我的测试文件如下:
#include <stdio.h>
#include<string.h>
int main()
{
FILE *fp0 = NULL;
char Buf[4096];
/*初始化Buf*/
strcpy(Buf,"Mem is char dev!");
printf("BUF: %s\n",Buf);
/*打开设备文件*/
fp0 = fopen("/dev/cdevdemo","r+");
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}
/*写入设备*/
fwrite(Buf, sizeof(Buf), 1, fp0);
/*重新定位文件位置(思考没有该指令,会有何后果)*/
fseek(fp0,0,SEEK_SET);
/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("BUF: %s\n",Buf);
/*读出设备*/
fread(Buf, sizeof(Buf), 1, fp0);
/*检测结果*/
printf("BUF: %s\n",Buf);
return 0;
}
使用GCC编译成可执行文件:
gcc test.c -o test
但是我们运行test程序却无法打开该设备文件,应该是权限不够。但是实际上我们的设备文件是可读的。。。但是就是运行测试文件无法得到预期结果,打开的文件指针一直为NULL。实际上我到的系统里面是存在该驱动的设备文件的——/dev/memdev0
我们的设备文件:
可能需要加上txt后缀?
/*打开设备文件*/
fp0 = fopen("/dev/cdevdemo.txt","r+");
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}
8.3.2.3、LDD程序的加载方式
模块的加载
通常来说,在驱动模块的开发阶段,一般是将模块编译成.ko文件,再使用
sudo insmod module.ko
或者
depmod -a
modprobe module
将模块加载到内核,相对而言,modprobe要比insmod更加智能,它会检查并自动处理模块的依赖,而insmod出现依赖问题时仅仅是告诉你安装失败,自己想办法吧。
LInux系统启动时,通过代码自身加载模块,这种方式称为静态编译入内核,驱动程序开发完毕一般使用该方式。
LInux系统启动后,通过insmod等命令来加载模块,这种方式称为动态加载,驱动程序开发调试过程一般使用这种方式。
模块编译进内核
我们要编译进入内核,需要使用内核的Makefile规则。创建对应的makefile文件。
可以参考博客:Linux设备驱动程序的编译进内核