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

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(使用前将#替换为@)

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

  • 在 execl 在输出中不可见之前打印

    include
  • 无法在 Linux 的 NetBeans 中编译 C++ 和 OpenGL (GLFW) 的简单源代码

    我开始学习 OpenGL glfw 我从教程中复制源代码并尝试编译它 但出现了错误 我想我已经正确安装了所有头文件 glm glfw 等 这是我的来源 我没有在头文件中使用这些字符 include iostream include stdi
  • 内核驱动程序从用户空间读取正常,但写回始终为 0

    因此 我正在努力完成内核驱动程序编程 目前我正在尝试在应用程序和内核驱动程序之间构建简单的数据传输 我使用简单的字符设备作为这两者之间的链接 并且我已成功将数据传输到驱动程序 但我无法将有意义的数据返回到用户空间 内核驱动程序如下所示 in
  • linux新手关于嵌入式linux设备驱动的问题

    最近在研究linux驱动 正如我读过的那些文章所说 设备驱动程序模块很可能会根据内核的需要自动加载 因此我想知道内核如何确定为特定设备 声卡 I2C spi 设备 等 我也无法彻底想象内核如何在启动时检测每个硬件设备 与嵌入式linux相关
  • 如果文件没有行尾字符,则 wc -l 不计算文件的最后一个

    我需要计算 unix 文件的所有行数 该文件有 3 行 但是wc l仅给出 2 个计数 我知道它不计算最后一行 因为它没有行尾字符 任何人都可以告诉我如何计算这一行吗 grep c返回匹配行的数量 只需使用一个空字符串 作为您的匹配表达式
  • 任何退出 bash 脚本但不退出终端的方法

    当我使用exitshell 脚本中的命令 该脚本将终止终端 提示符 有什么方法可以终止脚本然后停留在终端中吗 我的剧本run sh预计通过直接获取或从另一个脚本获取来执行 编辑 更具体地说 有两个脚本run2 sh as run sh ec
  • Vagrant 遇到问题 - “404 - 未找到”

    我正在尝试使用 Vagrant 制作一个 LAMP 盒子 有人告诉我它使用起来非常简单 我对网络和虚拟机完全陌生 对 Linux Ubuntu 的经验也很少 我目前已尝试按照官方文档页面上的教程进行操作 http docs vagrantu
  • 在 scapy 中通过物理环回发送数据包

    我最近发现了 Scapy 它看起来很棒 我正在尝试查看 NIC 上物理环回模块 存根上的简单流量 但是 Scapy sniff 没有给出任何结果 我正在做的发送数据包是 payload data 10 snf sniff filter ic
  • Docker忽略limits.conf(试图解决“打开文件太多”错误)

    我正在运行一个 Web 服务器 该服务器正在处理数千个并发 Web 套接字连接 为了实现这一点 在 Debian linux 我的基本镜像是 google debian wheezy 在 GCE 上运行 上 打开文件的默认数量设置为 100
  • Linux shell 从用户输入中获取设备 ID

    我正在为一个程序编写安装脚本 该程序需要在其配置中使用 lsusb 的设备 ID 因此我正在考虑执行以下操作 usblist lsusb put the list into a array for each line use the arr
  • LINUX:如何锁定内存中进程的页面

    我有一个 LINUX 服务器 运行一个具有大量内存占用的进程 某种数据库引擎 该进程分配的内存太大 需要将其中一部分换出 换出 我想做的是将所有其他进程 或正在运行的进程的子集 的内存页面锁定在内存中 以便只有数据库进程的页面被换出 例如
  • Python 3.4.3 subprocess.Popen 在没有管道的情况下获取命令的输出?

    我试图将命令的输出分配给变量 而不让命令认为它正在通过管道传输 原因是 如果正在通过管道传输 则相关命令会给出未格式化的文本作为输出 但如果从终端运行 则会给出颜色格式化的文本 我需要获取这种颜色格式的文本 到目前为止我已经尝试了一些事情
  • Linux 使用 boost asio 拒绝套接字绑定权限

    我在绑定套接字时遇到问题 并且以用户身份运行程序时权限被拒绝 这行代码会产生错误 acceptor new boost asio ip tcp acceptor io boost asio ip tcp endpoint boost asi
  • 如何在 Linux 上通过 FTP 递归下载文件夹 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 为什么 fork 炸弹没有使 android 崩溃?

    这是最简单的叉子炸弹 我在许多 Linux 发行版上执行了它 但它们都崩溃了 但是当我在 android 终端中执行此操作时 即使授予后也没有效果超级用户权限 有什么解释为什么它没有使 Android 系统崩溃吗 一句话 ulimit Li
  • 批量删除文件名中包含 BASH 中特殊字符的子字符串

    我的目录中有一个文件列表 opencv calib3d so2410 so opencv contrib so2410 so opencv core so2410 so opencv features2d so2410 so opencv
  • 执行命令而不将其保留在历史记录中[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 在进行软件开发时 经常需要在命令行命令中包含机密信息 典型示例是将项目部署到服务器的凭据设置为环境变量 当我不想将某些命令存储在命令历史记
  • SSH,运行进程然后忽略输出

    我有一个命令可以使用 SSH 并在 SSH 后运行脚本 该脚本运行一个二进制文件 脚本完成后 我可以输入任意键 本地终端将恢复到正常状态 但是 由于该进程仍在我通过 SSH 连接的计算机中运行 因此任何时候它都会登录到stdout我在本地终
  • 如何使用 JSch 将多行命令输出存储到变量中

    所以 我有一段很好的代码 我很难理解 它允许我向我的服务器发送命令 并获得一行响应 该代码有效 但我想从服务器返回多行 主要类是 JSch jSch new JSch MyUserInfo ui new MyUserInfo String
  • FileOutputStream.close() 中的设备 ioctl 不合适

    我有一些代码可以使用以下命令将一些首选项保存到文件中FileOutputStream 这是我已经写了一千遍的标准代码 FileOutputStream out new FileOutputStream file try BufferedOu

随机推荐

  • 算法:反转链表 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 内存