Linux内核结构与字符设备驱动

目录

用户态

内核态

系统调用接口

设备驱动程序

如何为设备找到对应的驱动程序

驱动链表

字符设备驱动框架

驱动的编译(往期博文Linux内核的编译)

用户态应用程序如何访问内核态框架程序

硬件级

 

d175c2087c1043d6bfd38c613612314a.png

用户态

  • 用户态由用户程序(C代码和调用的C函数库)和函数库(提供应用程序支配内核干活的接口)组成。
  • 通过函数库来访问系统调用接口来支配内核干活,进程间通信、进程调度、内存管理、文件操作、创建子进程等等。

内核态

系统调用接口

系统调用接口里有vfs、异常处理等功能体

  • vfs使得不管是什么文件类型,不管是磁盘文件还是设备,都能看作是文件,使得其只用open、write、read进行统一操作。
  • 异常处理根据发生异常的原因来调用不同的处理函数,如open()产生的异常就调用sys_open()、write()产生的异常就调用sys_write()、read()产生的异常就调用sys_read()

设备驱动程序

驱动一般分为字符设备驱动、块设备驱动、网络设备驱动。

如何为设备找到对应的驱动程序

用户态的应用程序调用open()传入进来的设备名(文件名)在内核态找到主设备号,从而找到驱动链表的相应驱动。

  • 设备号:主设备号(手机种类)与次设备号(手机型号)
  • 一个字符设备或者块设备都有一个主设备号和次设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备,即可以使用同一个主设备号,不同的次设备号

驱动链表

驱动链表管理着所有设备的驱动,驱动插入驱动链表的顺序由设备号检索,驱动链表主要有两个操作

  • 添加:编写完驱动程序,加载到内核,需要设备名(文件名)、设备号(确认该驱动在驱动链表的位置)、设备驱动函数(该函数操作寄存器来操作I/O口)
  • 查找:调用驱动程序,用户态调用open()

字符设备驱动框架

  1. 必要的头文件
  2. file_operations类型结构体,该结构体在内核加载驱动时会加入到驱动链表中,当用户态的应用程序调用open()时,就是根据传进来的设备名(文件名)来找到该接头体,让用户态应用程序的open、write、read函数与驱动程序的open、write、read产生联系
  3. open、write、read函数(该驱动框架的功能)
  4. 一些必要的宏
  5. 驱动的入口和出口函数

字符设备驱动框架

//驱动步骤1,必要的头文件
#include <linux/fs.h>      //file_operations声明
#include <linux/module.h>  //module_init  module_exit声明
#include <linux/init.h>    //__init  __exit 宏定义声明
#include <linux/device.h>  //class  devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h>   //设备号  dev_t 类型声明
#include <asm/io.h>        //ioremap iounmap的头文件

static struct class *first_drv_class;            //创建的类
static struct class_device *first_drv_class_dev; //创建的设备

//驱动步骤3,驱动框架的功能
// open函数
static int first_drv_open(struct inode *inode, struct file *file)
{
    printk("first_drv_open\n"); //内核的打印函数和printf类似
    return 0;
}

// write函数
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    printk("first_drv_write\n"); //内核的打印函数和printf类似
    return 0;
}

//驱动步骤2,file_operations结构体
//将上面的函数赋值给一个结构体中,方便下面加载到到驱动链表中去
static struct file_operations first_drv = {
    .owner = THIS_MODULE,
    .open = first_drv_open,
    .write = first_drv_write,
};

static int major;

//驱动步骤5,驱动的入口和出口函数
static int first_drv_init(void) //真实的驱动入口
{
    //register_chrdev(主设备号,设备名,file_operations结构体地址),当主设备号为0时,系统会自动分配一个主设备号,并作为该函数的返回值
    major = register_chrdev(0, "haozige", &first_drv);             //注册驱动,驱动名为haozige,告诉内核,把这个驱动加入到内核驱动的链表中
    first_drv_class = class_create(THIS_MODULE, "haoizge_class");  //创建一个类,类名:haozige_class,该类存在/sys/class底下
    
    //创建设备文件,在类的下面创建一个设备,设备名:hzg,设备在/sys/class/haozige_class底下,同时mdev机制会自助创建设备节点,即在/dev底下创建这个设备,即/dev/hzg
    first_drv_class_dev = class_device_create(first_drv_class, NULL, MKDEV(major, 0), NULL, "hzg"); //MKDEV(主设备号,次设备号)
    return 0;
}

static void first_drv_exit(void)
{
    class_device_unregister(first_drv_class_dev); //销毁设备
    class_destroy(first_drv_class);             //销毁类
    unregister_chrdev(major,"haozige");      //卸载驱动
}

//驱动步骤4,一些必要的宏
module_init(first_drv_init); //入口,内核加载驱动的时候,这个宏会被调用,告诉内核我的驱动入口函数是这个
module_exit(first_drv_exit); //出口,内核卸载驱动的时候,这个宏会被调用,告诉内核我的驱动出口函数是这个
MODULE_LICENSE("GPL");

字符设备驱动框架(pin21_drive.c)

#include <linux/fs.h>	   //file_operations声明
#include <linux/module.h>  //module_init  module_exit声明
#include <linux/init.h>	   //__init  __exit 宏定义声明
#include <linux/device.h>  //class  devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h>   //设备号  dev_t 类型声明
#include <asm/io.h>		   //ioremap iounmap的头文件

static struct class *pin21_class;	   //创建的类
static struct device *pin21_class_dev; //创建的设备

static dev_t devno;					//设备号,devno是用来接收创建设备号函数的返回值,销毁的时候需要传这个参数
static int major = 130;				//主设备号
static int minor = 4;				//次设备号
static char *module_name = "pin21"; //设备名

// open函数
static int pin21_open(struct inode *inode, struct file *file)
{
	printk("pin21_open\n"); //内核的打印函数和printf类似
	return 0;
}

// write函数
static ssize_t pin21_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	printk("pin21_write\n"); //内核的打印函数和printf类似
	return 0;
}

// read函数
static ssize_t pin21_read(struct file *file1, char __user *buf1, size_t count1, loff_t *ppos1)
{
	printk("pin21_read\n"); //内核的打印函数和printf类似
	return 0;
}

//将上面的函数赋值给一个结构体中,方便下面加载到到驱动链表中去

static struct file_operations pin21_fops = {
	// static 限定某些结构体或函数或变量仅在当前文件有效
	.owner = THIS_MODULE,
	.open = pin21_open,
	.write = pin21_write,
	.read = pin21_read,
};

int __init pin21_drv_init(void) //真实的驱动入口
{

	int ret;
	devno = MKDEV(major, minor);												  // 2. 创建设备号
	ret = register_chrdev(major, module_name, &pin21_fops);						  // 3. 注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中
	pin21_class = class_create(THIS_MODULE, "haozige_pin21");					  //在/dev下自动生成设备,创建一个类
	pin21_class_dev = device_create(pin21_class, NULL, devno, NULL, module_name); //创建设备文件,在类的下面创建一个设备。
	return 0;
}

void __exit pin21_drv_exit(void)
{

	device_destroy(pin21_class, devno);	   //销毁设备
	class_destroy(pin21_class);			   //销毁类
	unregister_chrdev(major, module_name); //卸载驱动
}

module_init(pin21_drv_init); //入口,内核加载驱动的时候,这个宏会被调用,告诉内核我的驱动入口函数是这个
module_exit(pin21_drv_exit); //出口,内核卸载驱动的时候,这个宏会被调用,告诉内核我的驱动出口函数是这个
MODULE_LICENSE("GPL v2");

驱动的编译(往期博文Linux内核的编译)

  • 驱动的编译需要一个编译好的内核,如该驱动是树莓派的驱动就需要一个编译好的树莓派Linux内核
  • 对于目标平台的Linux内核的编译,一般选择交叉编译的方式

 字符设备驱动的编译

  • 使用tree命令查看Linux内核源码目录结构
  • 将字符驱动程序放到Linux内核源码目录下的drivers/char底下bbc881f2ea7b4641a57c88e8829dc10a.png
  •  修改Makefile文件,将内核驱动程序编译成模块或者编译进内核,将字符驱动程序(pin21_driver.c)编译成模块 obj-m          +=pin21_driver.o fc7bf006384b4dea930020fb3a121034.png
  • 在Linux内核源码目录底下进行驱动编译,产生.ko文件,ARCH=armCROSS_COMPILE=arm-linux-gnueabinf- make modules121e0260784a40f790eab4ae8f87eb4d.png
    ARCH:指定架构CROSS_COMPILE:指定编译器
  •  将驱动程序加载到目标平台(树莓派)中,命令sudo insmod pin21_driver.ko5249754fea4148ea8b84ad2d1723634b.jpeg
  • 为该设备添加权限,命令sudo chmod 666 /dev/pin21
    44ff4807979d48edbda134457b1ee417.jpeg
  • 显示载入树莓派的驱动,命令lsmod
    ebf1babdd5794608a65f7f9fa3a1dffa.jpeg
  • 卸载驱动,命令sudo rmmod pin4 driver
    964603c5831e46be808559afd895c606.jpeg

用户态应用程序如何访问内核态框架程序

  • 当用户态(上层应用)调用open(“/dev/xxx”,O_RDWR,0666)时,会执行一条汇编指令(swi xxx),该指令会发生一次异常,类似于中断,中断号为0x80,0x80代表发生了一次系统调用,进入了内核态,然后内核态会调用syscall(),该函数会根据open()提供的设备名(文件名)找到主设备号从而在驱动链表找到相应的驱动(即找到结构体file_operations),然后系统调用接口的异常处理函数调用sys_open来调用驱动的kernel_open(),write(),read()同理,syscall()是由汇编实现的。
  • 系统调用接口的异常处理函数就会处理该条异常,根据发生异常的原因来调用不同的处理函数,如open()产生的异常就调用sys_open()、write()产生的异常就调用sys_write()、read()产生的异常就调用sys_read()

硬件级

设备:鼠标、键盘、led、屏幕、flash、内存、网卡等等

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值