内核功能
- ① 多任务管理
- ② 内存管理
- ③ 文件管理
- ④ 网络协议栈
- ⑤ 设备管理---设备驱动
----------------------------------------------我是分割线------------------------------------------------------------
-
一、内核开发特点
-
① 内核代码的运行时机
- 启动目标硬件
- 应用程序调用系统调用
- 硬件中断调用中断处理
- Linux内核和驱动间的接口(kABI)不稳定
- 内核升级后,接口可能会发生变化(以往驱动指针访问的结构体可能会存在异常)
- 将驱动写入内核更加稳定
- 二进制驱动需要匹配内核版本号才可运行,尽量选择longterm版本
- Andord用的是LTS内核
-
② 与应用层代码的区别
- 内核的大部分代码是被动执行的
- 32位的内核代码能够访问3G-4G的虚拟地址空间。64的操作系统则可以访问内核代码虚拟地址为4G-8G。
- 与C库的代码不一样,无法使用C标准库进行系统调用。内核将C库的代码重新再写了一遍,比如printf ->printk(但不完全一样)
- 内核代码中尽量不要使用浮点数,因为它会访问所有内容,影响代码的性能和移植行。
-
③ 与裸机代码的区别
- 内核代码使用虚拟地址,裸机代码使用物理地址
- 操作设备时需要按照内核的框架编写代码
-
④ 内核编译流程
- 导出环境变量 、选择内核架构、添加交叉编辑工具链、导入默认配置、导入当前芯片的内核架构默认配置、修改默认配置
-
二、内核模块特点
- 定义:将内核一部分代码编译到单独的目标文件,需要调用的时候再加载。(uname -r获取内核模块的信息)【使用内核模块的话,很多设备厂商可以独立去发布一些驱动代码】
- 文件目录分级:net下是文件协议栈;driver下是驱动信息。
- 大部分设备驱动可以被编译成内核模块
- 可以减少内存的占用,不用的时候不加载,需要时候再移入内存。同样也减少了内核的启动时间。
- 进行驱动升级和修复时,方便操作。同时重新上架的时候可减少内核的重启次数。
-
如何编译内核模块
-
① 内部模块(内核源码自带的模块)
- 先make menuconfig 编辑时将需要的内容配置为M, *是与内核一起编译
- make modules -j2(用两个处理器进行编译内部模块)
-
② 外部模块(第三方厂家开发的内核模块)
先建立一个目录,在内部创建一个类似Makefile的文件(Kbuild)。
在Kbuild文件内需要输入:obj-m = test.o(自己创建的.c文件的二进制)。
解释:最终编译的模块源文件来自于哪一个.c文件。
make -C 内核路径 M=模块路径 modules 【make -C /home/linux/linux M=`pwd` modules】
解释:利用内核makefile编译外部模块
建立多个:obj-m = hello.o (模块) 修改Kbuild文件,模块名不能和C文件名重复,增加模块名-objs 规则。
hello-objs = test.o func.o (依赖)
-
如何使用内核模块
- ① 在.c文件里敲内核模块功能,将其用内核进行编译
- ② 安装内核模块至根文件系统
- ③ 去目标端进行寻找内核模块
- ④ 查看已加载的内核模块(若代码编译为模块,需要提供入口函数)
设备驱动与模块参数
-
① 模块加载的时候可以传递参数,参数类型只能是以下几种
* Standard types are:
* byte, short, ushort, int, uint, long, ulong
* charp: a character pointer 一个字符型的指针
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
-
② 模块代码中使用module_param宏获取参数。
- 可以使用MODULE_PARM_DESE宏来设置模块参数的帮助信息。使用前可以用modinfo命令查看模块支持的参数。
- 可以通过sysfs虚拟文件系统访问模块参数(放在/sys/module/模块名/parameters目录中),可以通过cat或echo读取和修改模块参数。
内核模块可以调用EXPROT_SYMBOL宏导出的函数,若调用未导出的函数,加载模块时会报错。
-
设备驱动
- 字符设备:按字节进行访问的设备(大部分不支持随机访问 如SPI)
- 块设备:按块进行访问的设备(可随机访问,使用文件接口)
- 网络设备:没有设备文件(利用socket接口进行访问)
- 设备号:
- 主设备号(高12位的设备号major dev): 区分设备类型 从设备号(低20位的):区分同类中不同的设备
- 一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
获取主设备号与从设备号的宏如下:
- 设备文件,设备文件本身不包含任何数据。索引节点(i-node)才是区分文件之间(通过设备号来找)的标识。文件名不是。
- s =serial 串口 sg0=sda b开头的块设备 p是有名管道 c是字符设备
- 设备文件本身不包含任何数据
- 创建设备文件命令 mknod+文件名+类型(c\b\p)+主设备号(%255)+从设备号
- 设备驱动流程如下:
-
字符设备的注册流程
- 申请设备号 采用函数如下
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数自左到右为: 【1、操作系统分配的主设备号(全局变量)】【2、申请的主设备号数量】【3、主设备号对应的驱动或从设备名称(显示在/proc/devices文件中)】【成功返回0,失败返回错误码】
- 创建cdev(字符设备管理)结构体对象
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
- 该cdev中结构体链表构成如下
struct list_head {
struct list_head* next; 指头的
struct list_head* prev; ☞尾巴的
};
初始化该链表函数:void INIT_LIST_HEAD(struct list_head *list);
头插该链表函数:void list_add(struct list_head *new, struct list_head *head);
尾插该链表函数:void list_add_tail(struct list_head *new, struct list_head *head);
删除该链表函数:void list_del(struct list_head *entry);
遍历该链表:list_for_each_entry(pos, head, member) {
...} pos:当前节点指针 head:链表头 member:链表节点在结构体的名称
通过结构体成员的地址计算当前结构体所在的外部结构体首地址:
#define container_of(ptr, type, member) ({ const struct list_head *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );}) ptr:链表当前节点地址 type:链表节点所在结构体地址 member:链表节点成员名称
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
- 初始化cdev对象,采用函数如下
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数自左到右为:【1、cdev对象的指针】【2、保存cdev对象指定的系统函数结构体的指针】
- 将cdev对象放入字符设备的哈希表中(cdev_map)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数自左到右为:【1、cdev对象的指针】【2、当前设备号】【3、当前设备(驱动)数量】
整体流程:
----------------------------------------------我是分割线---------------------------------------------------------
-
字符设备文件读取内容(Read)
ssize_t test_read(struct file * filp, char __user * buf, size_t count, loff_t * pos)
buf:看一看用户需要读取的目标设备文件是什么,然后写入buf,发送给用户
count:一次读取多少内容给用户
pos:来自于vfs层级的偏移指针,不受此函数程序的影响。可用来增加实际读取的字节数
返回值:文件结束返回0
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
等同于memcpy,但会用来检查用户空间是否有效(将内核空间处的内容拷贝至用户空间)
to:用户空间的地址
from:内核的空间地址
n:复制的字节数
返回值:未赋值的字节数
int test_release(struct inode * A, struct file *B);
A:索引节点 B:已打开的文件
进程退出时,操作系统会自动关闭已打开的文件。
-
字符设备文件写入内容(write)
ssize_t test_write(struct file * filp, const char __user * buf, size_t count,
loff_t * pos)
参数从左到右分别为:【1、打开的文件】【2、写入数据的地址(__user宏来检查指针是否在用户空间
的0-3G)因为调用的是write,所以内核处write用get_user(内核变量,用户空间指针)来接收))】【3、本次需要写入的字节数】
【4、从个位置哪里开始写】
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
等同于memcpy,从内存拷贝。不限数据类型,但不一样的是这个函数会检查用户空间内存的有效性(将用户空间处的内容拷贝至内核空间)
参数从左到右为【1、拷贝至哪个对象的地址】【2、拷贝来源】【3、复制的字节数】【返回未拷贝的字节数】
void *ioremap(phys_addr_t offset, unsigned long size)
帮助内核访问物理地址处的外设寄存器函数,需要先将物理地址映射为虚拟地址。再通过虚拟地址修改寄存器
offset:物理地址 size:映射的内存大小 返回值:虚拟地址
int test_release(struct inode * A, struct file *B);
A:索引节点 B:已打开的文件
进程退出时,操作系统会自动关闭已打开的文件。
- 用户空间调用open函数打开某个设备文件后,在进程中会为设备文件分配一个未使用过的文件描述符,并且生成一个空白struct file结构体,然后从文件系统中查找到文件对应的inode(自己在文件系统中创建的设备节点)然后把该inode的i_fop赋值给进程中的struct file的f_op,也就是说此时进程已经找到了设备节点了,但是还没有调用我们自己写的驱动,即真正的file_operation接口。接下来为了调用真正的file_operation接口,我们从内核哈希表cdev_map中,根据设备号查找自己注册的sturct cdev(字符设备),获取cdev中的file_operation接口,把cdev中的file_operation接口赋值给struct file的f_op,此时进程已经可以最终调用自己实现的file_operation接口中的open函数,至此进程获得了该设备文件的自己实现的读写等操作。
---------------------------------------------------------我是分割线--------------------------------------------------------
伪字符设备
- 定义:没有实际物理硬件的设备
- 常见的伪字符设备: 1、/dev/null:空设备,丢弃写入的任何设备。2、/dev/zero:读出的数据全为0。3、/dev/random:读出的数据为随机值。
内存管理
- 在ARM阶段开发时,操作地址为物理地址。在应用层开发时地址为虚拟地址。(映射关系)
MMU(内存管理单元):负责翻译CPU过来的信号,翻译成物理地址。根据字典(页表)进行翻译。
页表保存了虚拟地址与物理地址的关系,进程的页表在内存中
工作流程:MMU查看虚拟地址是否在自己缓冲区(TLB)->若在直接使用,不存在去读取页表(内存中读取)->进行翻译
物理内存实际上是在使用时才给分配的。
- 进程创建的内核会为每个进程分配一个大页表。当进程分配内存时,内核会修改进程页表加入新的页表项。
- 进程切换时会切换大页表,因为随着进程的切换,虚拟地址到物理地址的映射已经改变。
- 分配小于一个页面(4k)的内存时,采用kmalloc函数
void *kmalloc(size_t size, gfp_t flags);
void kfree(void *p);
size:申请内存的大小 flags:标志(GFP_KERNEL:内核代码使用)
返回值:分配的内存地址
其分配物理地址和虚拟地址全部连续,一般用于分配小内存
void *vmalloc(unsigned long size);
void vfree(const void *addr);
vmalloc一般用于较大内存的分配,比如内核模块的加载,给其分配一个物理内存。
限制大小[128M] 分配的虚拟地址连续,物理不一定连续,所以要给其虚拟地址转化一个页表。故其分配内存效率低于kamlloc
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
gfp_mask:标志
order:申请页面个数的对数 1 = 2^0,2 =2^1 (0-11)
返回值:分配页面的首地址(虚拟地址)
其分配的物理地址和虚拟地址全部连续,一般分配一个页面(4kb)的大小
----------------------------------------------------------我是分割线-------------------------------------------------
设备模型(平台总线驱动)
关于总线使用此类结构体定义
static struct bus_type vbus = {
.name = "vbus", //重构此结构体名字为vbus
.match = vbus_match, //总线进行搭建驱动和设备的结构体函数
};
关于设备使用此类结构体定义
static struct device vdev={
.init_name = "vdev", 重构此结构体名字为vdev
.bus = &vbus, 外部引入总线的结构体,以便使用match找寻驱动
.release = vdev_release, 连接成功后释放此结构体的内容
};
关于驱动使用此类结构体定义
static struct device_driver vdrv ={
.name = "vder" ,
.bus = &vbus,
};
- 平台总线:虚拟的总线,不属于其他总线的设备,可以挂载到平台总线下。
- 平台设备:CPU可以控制的,属于平台总线的设备。
- struct resource(存放内容的)
- PC机的bios界面有一个自检表(ACPI),会收集外设信息。服务器平台使用uefi加载garbal来收集硬件信息。嵌入式产品一般使用设备树。
- 平台总线进行操作匹配平台设备、平台驱动时有四个方法。
- Intel的设备寄存器和内存的访问方法是不一样的,ARM中访问寄存器和内存是一样的。
-
设备和驱动匹配的种类
- 1.使用设备树节点的compatible属性进行匹配。 2.根据BIOS中的ACPI表信息进行匹配驱动 3.根据设备的ID进行匹配。4.根据设备的名称匹配驱动。
-
设备与匹配驱动的流程
- 定义平台设备的结构体变量(struct device filsd),将其添加至平台总线(struct bus_type vbus)。
- 定义平台驱动的结构体变量(struct device_driver vdrc),将其添加至平台总线(struct bus_type vbus)。
- 总线遍历所有设备,调用match函数绑定设备与驱动。
- 调用驱动的probe函数对设备进行初始化
- probe函数的处理:1,注册设备号(dev_t dev)。2,分配字符设备管理(cdev)对象并注册至哈希表。3,从设备结构体(struct device filsd)中获取寄存器的地址。4,利用iomap将操作寄存器地址映射到虚拟地址作为基准。
- 具体probe初始化设备函数源码如下:
static int fsled_probe(struct platform_device *pdev)
{
int ret; 判断设备号是否注册至系统的变量
dev_t dev; 获取的设备号
struct fsled_dev *fsled; 此结构体中的con成员用于定义硬件寄存器的虚拟地址
struct resource *res; 用于保存硬件寄存器的物理地址
//从设备信息中获取引脚编号
unsigned int pin = *(unsigned int*)pdev->dev.platform_data;
//使用LED的ID作为次设备号,生成设备号
dev = MKDEV(FSLED_MAJOR, pdev->id);
//注册设备号
ret = register_chrdev_region(dev, 1, FSLED_DEV_NAME);
if (ret)
goto reg_err;
//分配cdev对象
fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);
if (!fsled) {
ret = -ENOMEM;
goto mem_err;
}
cdev_init(&fsled->cdev, &fsled_ops);
fsled->cdev.owner = THIS_MODULE;
//将cdev对象添加到字符设备哈希表中
ret = cdev_add(&fsled->cdev, dev, 1);
if (ret)
goto add_err;
//从设备信息中获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
ret = -ENOENT;
goto res_err;
}
fsled->con = ioremap(res->start, resource_size(res)); 将寄存器的物理地址映射为虚拟地址
if (!fsled->con) {
ret = -EBUSY;
goto map_err;
}
//计算数据寄存器地址
fsled->dat = fsled->con + 1;
fsled->pin = pin;
//只允许一个进程打开设备文件
atomic_set(&fsled->available, 1);
//配置GPIO工作模式为输出
writel((readl(fsled->con) & ~(0xF << 4 * fsled->pin)) | (0x1 << 4 *
fsled->pin), fsled->con);
//设置GPIO引脚输出低电平
writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
//将cdev对象的指针保存到设备信息中,在remove函数中释放内存
platform_set_drvdata(pdev, fsled);
return 0;
map_err:
res_err:
cdev_del(&fsled->cdev);
add_err:
kfree(fsled);
mem_err:
unregister_chrdev_region(dev, 1);
reg_err:
return ret;
}
子系统(bus总线)的作用
- 设备文件的创建规则可能是依赖于设备属性,所以在创建设备文件时需要特别创建目录,然后在目录下生成设备文件。
- 设备进入操作系统----->总线识别并添加设备信息---->总线利用match函数加载驱动信息绑定该设备(此时已调用class_create建立设备目录在sysfs下)------>初始化该设备 (内核发出创建设备文件信号给上层子系统)------>在子系统生成的目录下,mdev用device-creat函数进行创建设备文件。
- 除读写之外的其他操作可以使用ioctl系统调用实现。
- udevd守护进程(嵌入式中使用mdev)创建设备文件,扫描规定系统目录,如果发生修改,就修改设备存放目录下的设备文件。
- 在驱动代码中,调用device_create函数在规定系统目录中生成对应的目录,再由udevd在设备存放目录下生成设备文件。