嵌入式内核及驱动开发中级(上)

2023-11-11

目录

第一部分

一. 设备分类、设备申请和注销

# 一、Linux内核对设备的分类

# 二、设备号------内核中同类设备的区分

# 三、申请和注销设备号

二.Code exerccise

三.知识补充

第二部分

一. 函数指针复习

# 一、函数指针复习

## 1.1、 内存四区

## 1.2、C语言中内存数据的访问方式

## 1.3、C语言中函数调用方式:

## 1.4 适用场合

第三部分

一. 注册字符设备

# 一、注册字符设备

#二. 小结

#三. 验证操作步骤

二. Code exercise

​​​​​​​三. 知识补充

第四部分

一. 字符设备驱动代码框架分析

# 一、字符设备驱动框架解析

## 1.1 两个操作函数中常用的结构体说明

## 1.2 字符设备驱动程序框架分析

## 1.3 参考原理图

## 1.4 常用操作函数说明

二. 知识补充

第五部分

一. 字符设备驱动读写操作实现

# 一、读操作实现

# 二、写操作实现

#三.设备操作函数避免使用全局变量

二. Code exercise

​​​​​​​三. 知识补充

第六部分

一. ioctl、printk、及一份驱动代码支持多个次设备

# 一、ioctl操作实现

# 二、printk

# 三、多个次设备的支持

二. Code exercise

三.知识补充


第一部分



一. 设备分类、设备申请和注销

# 一、Linux内核对设备的分类

linux的文件种类:

1. -:普通文件

2. d:目录文件

3. p:管道文件

4. s:本地socket文件

5. l:链接文件

6. c:字符设备

7. b:块设备

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存

2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节(4k),随机访问,设缓存以提高效率

3. 网络设备:针对网络数据收发的设备

总体框架图:

![Linux系统驱动总体框图](.\Linux系统驱动总体框图.png)

该图解决了应用程序的开发不同的设备使用同一套API对其操作, 减轻上层应用开发的压力。其本质是不管你是

什么类型的设备, 都是使用read, write这一套接口。

# 二、设备号------内核中同类设备的区分

内核用设备号来区分同类里不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:

1. 主设备号:占高12位,用来表示驱动程序相同的一类设备

2. 次设备号:占低20位,用来表示一类设备中的具体哪一个设备

应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。

MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:

```
dev_t devno;
int major = 251;//主设备号
int minor = 2;//次设备号
devno = MKDEV(major,minor);
```

MAJOR宏用来从32位设备号中分离出主设备号,用法:

```
dev_t devno = MKDEV(249,1);
int major = MAJOR(devno);
```

MINOR宏用来从32位设备号中分离出次设备号,用法:

```
dev_t devno = MKDEV(249,1);
int minor = MINOR(devno);
```

如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:

```

@ cd /dev

@ mknod 设备文件名 设备种类(c为字符设备,b为块设备)  主设备号  次设备号    //ubuntu下需加sudo执行

```

在应用程序中如果要创建设备可以调用系统调用函数mknod,其原型如下:



```

int mknod(const char *pathname,mode_t mode,dev_t dev);

pathname:带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下
mode:文件权限 位或 S_IFCHR/S_IFBLK

dev:32位设备号

返回值:成功为0,失败-1

```

# 三、申请和注销设备号

字符驱动开发的第一步是通过模块的入口函数向内核添加本设备驱动的代码框架,主要完成:

1. 申请设备号

2. 定义、初始化、向内核添加代表本设备的结构体元素

```
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:
	from:自己指定的设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
	成功为0,失败负数,绝对值为错误码
```

```
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
	dev:分配设备号成功后用来存放分配到的设备号
	baseminior:起始的次设备号,一般为0, 次设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
	成功为0,失败负数,绝对值为错误码
```

分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息

```
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
	from:已成功分配的设备号将被释放
	count:申请成功的设备数量
```

释放后/proc/devices文件对应的记录消失

二.Code exerccise

1.申请设备号

2.释放设备号

三.知识补充

  • 虚拟文件系统的本质也是一套软件, 向应用程序的开发人员提供了一套统一的简单的文件操作函数接口, 这样面对不同的设备的时候上层的开发用到的同一套api, 减轻开发压力。
  • 查看设备信息cat /proc/devices | grep name
  • 软连接和硬链接文件, 软连接, 其文件内容为路径名, 硬链接为源文件的不同文件名的表现形式。
  • 使用相关函数申请到设备号之后, 可以mknod创建代表这个设备的文件。

第二部分



一. 函数指针复习

# 一、函数指针复习

内存的作用-----用来存放程序运行过程中的

1. 数据, 全局变量

2. 指令:代码编译之后生成的机器码

## 1.1、 内存四区

堆区

栈区

全局区(静态区):全局变量, 静态变量, 常量

代码区:存放指令的地方

## 1.2、C语言中内存数据的访问方式

直接访问:通过所在空间名称去访问

间接访问:通过所在空间首地址去访问      \*地址值  此时的\*为间接访问运算符

## 1.3、C语言中函数调用方式:

直接调用:通过函数名去调用函数

间接调用:通过函数在代码区所对应的那份空间的首地址去调用

```c

int func(int a,int b)

{

    //......

}

int (int a,int b)  * pf;//语法错误

int *pf(int a,int b);//函数声明语句

int (*pf)(int a,int b);//定义一个函数指针

pf = &func;//&运算符后面如果是函数名的话可以省略不写

pf = func;

y = func(3,4);//直接调用

y = (*pf)(3,4);//间接调用,*运算符后面如果是函数指针类型则可以省略不写

y = pf(3,4);//间接调用, 语法糖的写法

typedef int myint;

typedef int (*)(int,int)  pft;//语法错误

typedef int (*pft)(int,int) ;     定义一个函数指针类型名

pft pt;  这样就定义了一个函数指针

```

## 1.4 适用场合

前提:当有很多个同类函数待被调用时

A处:知道所有函数名,由此处来决定B处将会调用哪个函数

B处:负责调用A处指定的函数

思考:A处如何告诉B处被调用的是哪个函数呢,无非两个办法:

1. 告诉B处函数名,怎么做呢?传字符串----“函数名”? C语言没有对应语法支持

2. 告诉B处对应函数在代码区的地址

在A处做一个函数指针数组, 将数组的首地址告诉B处, 让B处的函数挨个访问数组的的元素。

第三部分



一. 注册字符设备

# 一、注册字符设备

linux内核用这样的一个结构体用于描述一个对象(设备)的信息:

```

struct cdev

{

struct kobject kobj;//表示该类型实体是一种内核对象, 父类

struct module *owner;//填THIS_MODULE,表示该字符设备从属于哪个内核模块

const struct file_operations *ops;//指向空间存放着针对该设备的各种操作函数地址

struct list_head list;     //链表指针域

dev_t dev;//设备号

unsigned int count;      //设备数量

};

```

自己定义的结构体中必须有一个成员为 struct cdev cdev,两种方法定义一个设备:

1. 直接定义:定义结构体全局变量

2. 动态申请:

kmalloc, vmalloc, get_free_page

3.函数操作集合

 /*将操作这个字符设备的函数集绑定到这个设备上*/

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



```c

struct file_operations (函数操作集合, 也叫做桩函数)

{

   struct module *owner;           //填THIS_MODULE,表示该结构体对象从属于哪个内核模块  

    int (*open) (struct inode *, struct file *);        //打开设备

   int (*release) (struct inode *, struct file *);        //关闭设备

   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);        //读设备

   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备

   loff_t (*llseek) (struct file *, loff_t, int);                //定位

   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备

   unsigned int (*poll) (struct file *, struct poll_table_struct *);        //POLL机制,实现多路复用的支持

   int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层

   int (*fasync) (int, struct file *, int); //信号驱动

   //......

};

```

该对象各个函数指针成员都对应相应的系统调用函数,应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数:

一般定义一个struct file_operations类型的全局变量并用自己实现各种操作函数名对其进行初始化



int cdev_add(struct cdev *p,dev_t dev,unsigned int count)

功能:将指定字符设备添加到内核 ,这些设备使用一个哈希链表管理起来的

参数:

p:指向被添加的设备

dev:设备号

count:设备数量,一般填1





void cdev_del(struct cdev *p)

功能:从内核中移除一个字符设备

参数:

#二. 小结

字符设备驱动开发步骤:

1.  如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev

2.  定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配, 动态分配完记得释放对应的空间)

3.  定义三个全局变量分别来表示主设备号、次设备号、设备数

4.  定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE

5.  module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE  e. 添加字符设备到内核

6.  module exit函数:a. 注销设备号 b. 从内核中移除struct cdev  c. 如果如果是全局设备指针则释放其指向空间

7.  编写各个操作函数并将函数名初始化给struct file_operations结构体变量

#三. 验证操作步骤

1. 编写驱动代码mychar.c

2. make生成ko文件

3. insmod内核模块

4. 查阅字符设备用到的设备号(主设备号):cat  /proc/devices  |  grep  申请设备号时用的名字

5. 创建设备文件(设备节点) : mknod   /dev/???   c   上一步查询到的主设备号    代码中指定初始次设备号

6. 编写app验证驱动(testmychar_app.c)

7. 编译运行app,dmesg命令查看内核打印信息

二. Code exercise

1.驱动代码

2.用户空间写一个app验证

三. 知识补充

打开这个设备文件就是对应的操作我们这个设备。

  • 任何一种语言都可以实现面向对象编程,只是说合适不合适, 方便不方便, 表现形式上面哪一种语言更容易让人接受。
  • linux内核使用的是C99的c标准。给结构体成员赋值的时候稍微有点不同。
  • 有了设备号以后去创建设备节点, 与设备对应的设备文

第四部分



一. 字符设备驱动代码框架分析

# 一、字符设备驱动框架解析

设备的操作函数如果比喻是桩的话(性质类似于设备操作函数的函数,在一些场合被称为桩函数),则:

驱动实现设备操作函数 ----------- 做桩

insmod调用的init函数主要作用 --------- 钉桩

rmmod调用的exitt函数主要作用 --------- 拔桩

应用层通过系统调用函数间接调用这些设备操作函数 ------- 用桩

## 1.1 两个操作函数中常用的结构体说明

```c

内核中记录文件元信息的结构体(与外存中的文件一一对应, 记录着一个文件的时间戳, 操作权限、、、)

struct inode

{

//....

dev_t  i_rdev;//设备号

struct cdev  *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象

//....

}

/*

1. 内核中每个该结构体对象对应着一个实际文件,一对一

2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建

3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)

*/

```

```c

读写文件内容过程中用到的一些控制性数据组合而成的对象------文件操作引擎(文件操控器)

struct file

{

//...

mode_t f_mode;//不同用户的操作权限,驱动一般不用

loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用, 使用指针描述实际上是不准确的。

unsignedintf_flags;//open时的第二个参数flags存放在此,驱动中常用

struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用

void*private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据

struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode

        int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象

//...

};

/*

1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作

2. open同一个文件多次,每次open都会创建一个该类型的对象

3. 文件描述符数组中存放的地址指向该类型的对象

4. 每个文件描述符都对应一个struct file对象的地址

*/

```

## 1.2 字符设备驱动程序框架分析

驱动实现端:

内核中使用着哈希链表来管理着struct cdev 这样的一个对象。

驱动使用端:

syscall_open函数实现的伪代码:

```c

int syscall_open(const char *filename,int flag)

{

    dev_t devno;

    struct inode *pnode = NULL;

    struct cdev *pcdev = NULL;

    struct file *pfile = NULL;

    int fd = -1;

   

    /*根据filename在内核中查找该文件对应的struct inode对象地址

        找到则pnode指向该对象

        未找到则创建新的struct inode对象,pnode指向该对象,并从文件系统中读取文件的元信息到该对象*/

    if(/*未找到对应的struct inode对象*/)

    {/*根据文件种类决定如何进行下面的操作,如果是字符设备则执行如下操作*/

   

            /*从pnode指向对象中得到设备号*/

    devno = pnode->i_rdev;

   

            /*用devno在字符设备链表查找对应节点,并将该节点的地址赋值给pcdev*/

   

            /*pcdev赋值给pnode的i_cdev成员*/

            pnode->i_cdev = pcdev;

    }

   

    /*创建struct file对象,并将该对象的地址赋值给pfile*/

   

    pfile->f_op = pnode->i_cdev->ops;

    pfile->f_flags = flag;

   

    /*调用驱动程序的open函数*/

    pfile->f_op->open(pnode,pfile,flag);

   

    /*将struct file对象地址填入进程的描述符数组,得到对应位置的下标赋值给fd*/

   

    return fd;

}

```

syscall_read函数实现的伪代码:

```c

int syscall_read(int fd,void *pbuf,int size)

{

    struct file *pfile = NULL;

    struct file_operations *fops = NULL;

    int cnt;

   

    /*将fd作为下标,在进程的描述符数组中获得struct file对象的地址赋值给pfile*/

   

    /*从struct file对象的f_op成员中得到操作函数集对象地址赋值给fops*/

   

    /*从操作函数集对象的read成员得到该设备对应的驱动程序中read函数,并调用之*/

    cnt = fops->read(pfile,pbuf,size,&pfile->f_pos);

   

    。。。。

    return cnt;

}

```

## 1.3 参考原理图

![字符设备驱动框架](.\字符设备驱动框架.jpg)

![Linux字符设备驱动工作原理图](.\Linux字符设备驱动工作原理图.png)

## 1.4 常用操作函数说明

```c

int (*open) (struct inode *, struct file *);        //打开设备

/*

指向函数一般用来对设备进行硬件上的初始化,对于一些简单的设备该函数只需要return 0,对应open系统调用,是open系统调用函数实现过程中调用的函数,

*/

int (*release) (struct inode *, struct file *);        //关闭设备

/*

,指向函数一般用来对设备进行硬件上的关闭操作,对于一些简单的设备该函数只需要return 0,对应close系统调用,是close系统调用函数实现过程中调用的函数

*/

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);        //读设备

/*

指向函数用来将设备产生的数据读到用户空间,对应read系统调用,是read系统调用函数实现过程中调用的函数

*/

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备

/*

指向函数用来将用户空间的数据写进设备,对应write系统调用,是write系统调用函数实现过程中调用的函数

*/

loff_t (*llseek) (struct file *, loff_t, int);                //数据操作位置的定位

/*

指向函数用来获取或设置设备数据的开始操作位置(位置指示器),对应lseek系统调用,是lseek系统调用函数实现过程中调用的函数

*/

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备

/*

指向函数用来获取、设置设备一些属性或设备的工作方式等非数据读写操作,对应ioctl系统调用,是ioctl系统调用函数实现过程中调用的函数

*/

unsigned int (*poll) (struct file *, struct poll_table_struct *);//POLL机制,实现对设备的多路复用方式的访问

/*

指向函数用来协助多路复用机制完成对本设备可读、可写数据的监控,对应select、poll、epoll_wait系统调用,是select、poll、epoll_wait系统调用函数实现过程中调用的函数

*/

  int (*fasync) (int, struct file *, int); //信号驱动

/*

指向函数用来创建信号驱动机制的引擎,对应fcntl系统调用的FASYNC标记设置,是fcntl系统调用函数FASYNC标记设置过程中调用的函数

*/

```

二. 知识补充

驱动实现端:假设现在有一个字符设备, 那么就需要编写该设备的相关的操作函数集(编写桩函数), 将这些函数都赋值给struct file_operations结构体中的对应成员, 在把这个struct file_operations对象的地址给struct cdev对象里面的struct file_operations *这个成员, 这样当拿到cdev这个对象的地址的时候就可以调到相关的操作函数。

应用层调用系统调用函数时: open函数的第一个参数时文件名, 调用该函数在内核中的open函数的定义会干这样一些事,会在inode这样一个对象构成的链表里面搜索有没有对应文件名的节点, struct inode该对象的成员主要是保存了外存的文件的元信息, 有该节点就将设备号和一些相关的信息赋值给struct inode这个对象的相应结构体里,使用inoe对象里面的设备号去描述一个设备的对象构成的哈希链表中去查找, 找到就把该节点的地址赋值给inode对象里面的strcut cdev *这个对象, 接着就可以把cdev对象里面的操作函数集的地址给strcut file对象里的struct file_operations *这个对象, open打开一个文件还会生成struct file*(文件操作引擎类型的数组)类型的数组, 将strcut file对象的地址存到数组中, 接着返回数组的下标。

应用层在调用read、write函数的时候, 就可以直接拿着文件描述符也就是数组的下表就可以找到struct file对象, 就可以使用该对象里面的函数操作集合的地址间接的调用到驱动程序的相关函数。

  • 每一个进程都包含着一个文件描述符数组
  • 作业:请问字符设备驱动框架是怎样的?(与应用层做对比)
  • 文件描述符的本质是文件操作引擎这种对象的指针数组的下标, 数组内容为其它文件操作引擎的地址。
  • 学习总结

第五部分



一. 字符设备驱动读写操作实现

# 一、读操作实现

```c

ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);

完成功能:读取设备产生的数据

参数:

    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open

    pbuf:指向用户空间一块内存,用来保存读到的数据

    count:用户期望读取的字节数

    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置

 返回值:

    本次成功读取的字节数,失败返回-1

```

put_user(x,ptr)

x:char、int类型的简单变量名

unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)

成功为返回0,失败非0

# 二、写操作实现

```c

ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos); 

完成功能:向设备写入数据

参数:

    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open

    pbuf:指向用户空间一块内存,用来保存被写的数据

    count:用户期望写入的字节数

    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置

 返回值:

    本次成功写入的字节数,失败返回-1

```

get_user(x,ptr)

x:char、int类型的简单变量名

unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)

成功为返回0,失败非0

#三.设备操作函数避免使用全局变量

解决办法:

将其保存到文件操作引擎的私有数据成员里面去, 这样不是函数操作集合里面的成员就不会轻易的拿到代表这个设备的结构体的地址。

已知结构体成员其中之一的地址,container_of宏求出结构体的首地址.

二. Code exercise

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>

#define KERNEL_BUF 100

//定义主次设备号和设备号数量
int major = 11;
int minor = 1;
int dev_num = 1;

//定义内核缓冲区,用来模拟真实设备的相应寄存器
char kernel_buf[KERNEL_BUF];
int curlen = 0;
struct cdev mydev;


int myopen(struct inode *pnode, struct file *pfile) {

	printk("myopen is called.\n");

	return 0;
}

int myclose(struct inode *pnode, struct file *pfile) {

	printk("myclose is called.\n");
	return 0;
}

ssize_t myread(struct file *pfile, char __user *user_buf, size_t count, loff_t *p_pos) {

	int real_size = 0;
	int ret = 0;

	if(count > curlen) {
		real_size = curlen;
	}
	else {
		real_size = curlen;
	}

	ret = copy_to_user(user_buf, kernel_buf, real_size);
	if(ret) {
		printk("copy_to_user failed.\n");
		return -1;
	}

	memcpy(kernel_buf, kernel_buf + real_size, curlen - real_size);
	curlen -= real_size;

	return real_size;
}

ssize_t mywrite(struct file *pfile, const char __user *user_buf, size_t count, loff_t *p_pos) {

	int ret = 0;
	int real_size = 0;

	if(count > KERNEL_BUF - curlen) {
		real_size = KERNEL_BUF - curlen;
	}
	else {
		real_size = count;
	}

	ret = copy_from_user(kernel_buf + curlen, user_buf, real_size);
	if(ret) {
		printk("copy_from_user failed.\n");
		return -1;
	}

	curlen += count;

	return real_size;
}

struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = myopen,
	.release = myclose,
	.read = myread,
	.write = mywrite
};



int __init mydev_init(void) {
	int ret;
	int dev_no = MKDEV(major, minor);

	ret = register_chrdev_region(dev_no, dev_num, "mydev");
	if(ret) {
		printk("register_chrdev_region failed.\n");

		ret = alloc_chrdev_region(&dev_no, minor, dev_num, "mydev");
		if(ret) {
			printk("alloc_chrdev_region failed.\n");
			return -1;
		}
		major = MAJOR(dev_no);
	}


	//bind ops to cdev
	cdev_init(&mydev, &myops);
	mydev.owner = THIS_MODULE;
	
	//向内核添加对应的设备描述对象
	cdev_add(&mydev, dev_no, dev_num);

	return 0;
	
}

int __exit mydev_exit(void) {

	int dev_no = MKDEV(major, minor);
	
	unregister_chrdev_region(dev_no, dev_num);

	cdev_del(&mydev);
	
	return 0;
}

MODULE_LICENSE("GPL");
module_init(mydev_init);
module_exit(mydev_exit);

三. 知识补充

1.所有用户加上写权限

2.container_of原理分析

container_of(已知成员的地址, 结构体类型, 已知成员在这个结构体里面叫什么)

//essence
已知成员的地址 - &((struct mychardev *)0->已知成员)

已知成员的地址 - 该成员在结构体中的偏移量()

第六部分



一. ioctl、printk、及一份驱动代码支持多个次设备

# 一、ioctl操作实现

已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称), 使用这个宏就可以拿到结构体的首地址, 内核源码内部的一个宏。

```c

long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);        //对设备属性的设置

功能:对相应设备做指定的控制操作(各种属性的设置获取等等)

参数:

filp:指向open产生的struct file类型的对象,表示本次ioctl对应的那次open, 文件操作引擎

cmd:用来表示做的是哪一个操作, 配合switch语句来使用。

       arg:和cmd配合用的参数

返回值:成功为0,失败-1

```

cmd组成

1. dir(direction),ioctl 命令访问模式(属性数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;

2. type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如 ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识

3. nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;

4. size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;

```c

#define _IOC(dir,type,nr,size) (((dir)<<_IOC_DIRSHIFT)| \

                               ((type)<<_IOC_TYPESHIFT)| \

                               ((nr)<<_IOC_NRSHIFT)| \

                               ((size)<<_IOC_SIZESHIFT))

/* used to create numbers */



// 定义不带参数的 ioctl 命令

#define _IO(type,nr)   _IOC(_IOC_NONE,(type),(nr),0)



//定义带读参数的ioctl命令(copy_to_user) size为类型名

#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))



//定义带写参数的 ioctl 命令(copy_from_user) size为类型名

#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))



//定义带读写参数的 ioctl 命令 size为类型名

#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))



/* used to decode ioctl numbers */

#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)

#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)

#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)

#define _IOC_SIZE(nr)      (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

```

# 二、printk

```c

//日志级别, 日志信息, 给程序员查看系统的运行情况的信息


#define        KERN_EMERG        "<0>"        /* system is unusable                        */
#define        KERN_ALERT        "<1>"        /* action must be taken immediately        */
#define        KERN_CRIT        "<2>"        /* critical conditions                        */
#define        KERN_ERR        "<3>"        /* error conditions                        */

#define        KERN_WARNING        "<4>"        /* warning conditions                        */
#define        KERN_NOTICE        "<5>"        /* normal but significant condition        */
#define        KERN_INFO        "<6>"        /* informational                        */
#define        KERN_DEBUG        "<7>"        /* debug-level messages                        */

用法:printk(KERN_INFO"....",....)
  

    printk(KERN_INFO"Hello World"); =====> printk("<6>""Hello World") ====> printk("<6>Hello World")

 
```


dmesg --level=emerg,alert,crit,err,warn,notice,info,debug


```c

#define HELLO_DEBUG

#undef PDEBUG

#ifdef HELLO_DEBUG

#define PDEBUG(fmt, args...) printk(KERN_DEBUG fmt, ##args)

#else

#define PDEBUG(fmt, args...)

#endif


```

# 三、多个次设备的支持

每一个具体设备(次设备不一样的设备),必须有一个struct cdev来代表它

cdev_init

cdev.owner赋值

cdev_add

以上三个操作对每个具体设备都要进行

(1)方法一:第一多个struct cdev对象来代表相应的此设备,

(2)方法二:对设备进行操作的时候最为主要的是区分对哪一个设备的缓冲区进行读写操作,具体实现是在open函数时通过区分inode节点中的设备号, 使用MINOR这个宏区分出此设备号进行对不同次设备缓冲区的指定并将其保存在file里面的private_data成员里面.

二. Code exercise

三.知识补充

1.为某个文件的所有用户添加些权限

2.文件打开出问题, 检查主次设备号对不对.

3.出现一下错误的原因是因为定义的结构体与你使用的结构体的名称不一样

4.错误分析

有进程正在使用这个模块, 解决办法重启内核, 或者lsmod查出依赖该模块的进程, 将其关闭。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌入式内核及驱动开发中级(上) 的相关文章

  • 为 Qt 应用程序创建 Linux 安装

    我刚刚用 Qt Creator 制作了一个很棒的程序 我对自己很满意 如何将其从台式机移至笔记本电脑 那么 最好的方法是安装程序 对吗 对于 Ubuntu 这是一个 Debian 软件包 对吗 我怎么做 有人这样做过吗 他们可以分享 QT
  • 如何在Python中独立于语言安装(linux)获取用户桌面路径

    我找到了 如何找到用户桌面的路径 的几个问题和答案 但在我看来它们都已失效 至少我找到的那些 原因是 如果用户安装的 Linux 不是英语 他或她的桌面很可能位于除 Desktop 例如 对于瑞典语 我相信它是在 Skrivbord 谁知道
  • 在 Ubuntu 16.04 上找不到 printf.c

    我最近切换到Ubuntu 16 04 我在用vscode作为 Ubuntu 上的 IDE 我配置了其他语言 但我无法做到这一点C C 我创建c cpp properties json launch json tasks json 当我开始编
  • Linux shell 脚本:十六进制数字到二进制字符串

    我正在 shell 脚本中寻找一些简单的方法来将十六进制数字转换为 0 和 1 字符的序列 Example 5F gt 01011111 是否有任何命令或简单的方法来完成它 或者我应该为其编写一些开关 echo ibase 16 obase
  • Bash 方法的返回值总是模 256

    我有一个 bash 脚本方法 它返回输入值 然而 返回值始终是模 256 的值 我用 google 搜索了一段时间 发现this http www tldp org LDP abs html exitcodes html文章说它总是以 25
  • 适用于 KDE 和 Gnome 的 Gui [重复]

    这个问题在这里已经有答案了 我想为一个现在是 CLI 的应用程序编写一个 gui 它需要在 KDE 和 Gnome DE 中 看起来不错 充分利用用户的外观设置 如果我选择 Qt 或 GTK 我能够做到这一点吗 它们与两个 DE 集成良好吗
  • 从多线程程序中调用 system()

    我们正在开发一个用 C 编写的多线程内存消耗应用程序 我们必须执行大量的 shellscript linux 命令 并获取返回码 读完之后article http www linuxprogrammingblog com threads a
  • 设置 Apache POI 的路径

    我想创建 Excel 文件并使用 java 程序在该文件中写入数据 That is here http www techbrainwave com p 554我在 java 文件所在的位置提取了 Apache POI 并将该路径包含在路径变
  • Linux shell 脚本中的 while 循环超时

    这工作正常 无限循环 while TRUE do printf done 我在尝试着timeout this while loop与timeout命令 所有这些都不起作用 timeout 5 while TRUE do printf don
  • ansible 重新启动 2.1.1.0 失败

    我一直在尝试创建一个非常简单的 Ansible 剧本 它将重新启动服务器并等待它回来 我过去在 Ansible 1 9 上有一个可以运行的 但我最近升级到 2 1 1 0 并且失败了 我正在重新启动的主机名为 idm IP 为 192 16
  • Unix 命令列出包含字符串但*不*包含另一个字符串的文件

    如何递归查看包含一个字符串且不包含另一个字符串的文件列表 另外 我的意思是评估文件的文本 而不是文件名 结论 根据评论 我最终使用了 find name html exec grep lR base maps xargs grep L ba
  • 修改linux下的路径

    虽然我认为我已经接近 Linux 专业人士 但显然我仍然是一个初学者 当我登录服务器时 我需要使用最新版本的R 统计软件 R 安装在 2 个地方 当我运行以下命令时 which R I get usr bin R 进而 R version
  • 使用 find - 删除除任何一个之外的所有文件/目录(在 Linux 中)

    如果我们想删除我们使用的所有文件和目录 rm rf 但是 如果我希望一次性删除除一个特定文件之外的所有文件和目录怎么办 有什么命令可以做到这一点吗 rm rf 可以轻松地一次性删除 甚至可以删除我最喜欢的文件 目录 提前致谢 find ht
  • 使用 grep 查找包含所有搜索字符串的行

    我有一个文件 其中包含很多与此类似的行 id 2796 some model Profile message type MODEL SAVE fields account 14 address null modification times
  • 应用程序无缘无故地被杀死。怀疑 BSS 高。如何调试呢?

    我已经在CentOs6 6中成功运行我的应用程序 最近 硬件 主板和内存 更新了 我的应用程序现在毫无理由地被杀死 root localhost PktBlaster PktBlaster Killed 文件和 ldd 输出 root lo
  • 如何在 Linux 中编写文本模式 GUI? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 当我编写脚本 程序时 我经常想弹出一个简单的文本 gui 来提示输入 我该怎么做 例如 来自 Shel
  • Jenkins中找不到环境变量

    我想在詹金斯中设置很多变量 我试过把它们放进去 bashrc bash profile and profile of the jenkins用户 但 Jenkins 在构建发生时找不到它们 唯一有效的方法是将所有环境变量放入Jenkinsf
  • 如何根据 HTTP 请求使用 Python 和 Flask 执行 shell 命令并流输出?

    下列的这个帖子 https stackoverflow com questions 15092961 how to continuously display python output in a webpage 我能够tail f网页的日志
  • 如何在 shell 脚本中并行运行多个实例以提高时间效率[重复]

    这个问题在这里已经有答案了 我正在使用 shell 脚本 它读取 16000 行的输入文件 运行该脚本需要8个多小时 我需要减少它 所以我将其划分为 8 个实例并读取数据 其中我使用 for 循环迭代 8 个文件 并在其中使用 while
  • 为什么 Linux perf 使用事件 l1d.replacement 来处理 x86 上的“L1 dcache misses”?

    在英特尔 x86 上 Linux用途 https stackoverflow com a 52172985 149138事件l1d replacements来实施其L1 dcache load misses event 该事件定义如下 计数

随机推荐

  • 算法:反转链表 java

    方法1 迭代 public static Node reverseNode Node head 前一个节点 Node pre null 当前节点 Node cur head 如果当前节点不为空 while cur null 存储下一个节点
  • 《数学建模实战攻略:引言》

    一 专栏简介与目标 欢迎来到 数学建模实战攻略 专栏 本专栏旨在帮助初学者 参加数学建模竞赛的学生以及对数学建模感兴趣的研究者和开发者全面了解数学建模的知识体系 掌握建模方法和技巧 提高解决实际问题的能力 本专栏将涵盖数学建模的基本概念 方
  • ALSA信息查看

    1 1 1 查看当前Soc的声卡状态 cat proc asound cards 例如 插入USB声卡之后 会新增声卡节点 USB声卡无声可优先查看该状态 1 1 2 查看当前声卡工作状态 声卡分两种通道 一种是Capture 一种是Pla
  • web的欢迎资源文件

    欢迎资源文件 1 前提 用户可以记住网站名 但是不会记住网站资源文件名 2 默认欢迎资源文件 用户发送了一个针对某个网站的 默认请求 时 此时由Http服务器自动从当前网站返回的资源文件 正常请求 http localhost 8080 m
  • 关于Electron 串口通讯serialport 打包的问题

    请教各位大佬一下 我使用serialport模块 程序编译后正常运行 但是无法打包 目前搞不清楚原因 特来请教 PS C Users appyjj Desktop cart gt npm run build gt cart 1 0 0 bu
  • 学会项目成本管理计算,PMP计算题就是送分题

    学会项目成本管理计算 PMP计算题就是送分题 PMP中的计算主要在 lt 项目成本管理 gt 的控制成本部分 服务于挣值管理 EVM Earned Value Management 挣值分析 EVA Earned Value Analysi
  • MySQL学习日记day04(索引、视图、DBA常用命令、数据库设计三范式)

    目录 一 索引 index 1 什么是索引 2 索引的实现原理 3 在mysql当中 主键上 以及unique字段上都会自动添加索引的 4 索引怎么创建 怎么删除 语法是什么 5 在mysql当中 怎么查看一个SQL语句是否使用了索引进行检
  • Node.js 下载安装环境配置 - 图文版

    Node js 是一个开源 跨平台的 JavaScript 运行时环境 一 介绍 1 官方文档 1 中文文档 Node js 中文网 2 英文文档 Node js 二 下载 1 中文 2 英文 编辑三 安装 1 新建一个文件夹作为安装路径
  • Win11家庭版怎么开启远程桌面

    Win11家庭是专为家庭用户准备的版本 由于Win11系统是一款全新的系统 很多用户对一些功能还不是很熟悉 那么Win11家庭版怎么开启远程桌面 下面就来看看详细教程 Win11家庭版开启远程桌面教程 1 首先 我们需要先下载安装一款远程桌
  • 黑马程序员--多线程

    黑马程序员 多线程 Java培训 Android培训 iOS培训 Net培训 期待与您交流 一 定义 进程 是一个正在执行中的程序 每一个进程执行都有一个执行顺序 该顺序是一个执行路径或叫控制单元 线程 进程中的一个独立的控制单元 线程在控
  • Ubuntu32位安装VSCODE

    Ubuntu32位安装VSCODE vscode自1 36版本后停止支持32位linux系统 所以要使用 lt 1 36版本 vscode所有版本下载地址 https code visualstudio com updates v1 33
  • ubuntu12.04搭建CUDA4.2开发环境

    实验室老师让调试一个DeepLearning的程序 叫做DropConnecte 必须要在64bit的Linux系统上运行 还要配置CUDA 本人比较笨 重装了好多次Ubuntu之后才整成功 特把整理的资料发到这里 1 动态链接库解决方案h
  • Ubuntu和vmware前期设置教程

    系统删除不要从左侧一栏删除 从菜单栏虚拟机 管理 从磁盘删除 ubuntu版本下载地址 清华镜像源 https mirrors tuna tsinghua edu cn ubuntu releases vmware tools 安装教程 h
  • YOLOv5-Lite 使用笔记

    目录 开源一些有用信息 推理部分代码提取出来 不依赖第三方库 c opencv onnx 推理
  • SQL-DAY 9(SQL应用案例:电商用户、商品、平台价值分析)

    文章目录 1 项目背景 2 使用 人货场 拆解方式建立指标体系 3 确认问题 4 准备工作 4 1 数据读取 用户行为数据 4 2 数据预处理 5 指标体系建设 5 1 用户指标体系 5 1 1 基础指标 5 1 2 RFM模型分析 5 2
  • 11月3日文章推荐

    文章目录 1 演讲 1 1 John Edward Hopcroft 开放科学 科学传播与人才培养 2 人物 2 1 John Edward Hopcroft 2 2 吴天齐 2 3 Johnson Kuan 2 4 陶中恺 3 新闻 3
  • MOS管应用---电源开关、电平转换、防反接、全桥变换器

    MOS管应用 电源开关 电平转换 防反接 全桥变换器 1 PMOS作电源开关 Q2也可以用光耦替代 电容C1 电阻R2延长MOS管导通 截止时间 实现软开启 soft start 功能 充电时间3到4个R2 C2 2 NMOS作双向电平转换
  • Java 对象序列化

    目录 一 序列化 定义 方法 代码 二 反序列化 定义 方法 代码 三 自定义类序列化 步骤 代码 一 序列化 定义 将内存中的Java对象保存到磁盘中或通过网络传输过去 方法 使用过ObjectOutputStream类实现 另外 Obj
  • Python——深拷贝与浅拷贝

    s1 你好 s2 s1 print id s1 print id s2 gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt gt
  • 嵌入式内核及驱动开发中级(上)

    目录 第一部分 一 设备分类 设备申请和注销 一 Linux内核对设备的分类 二 设备号 内核中同类设备的区分 三 申请和注销设备号 二 Code exerccise 三 知识补充 第二部分 一 函数指针复习 一 函数指针复习 1 1 内存