13.1读《linux设备驱动程序》笔记一

第二章:构造和运行模块

1.只有授权用户(超级用户)才能装载模块

2.驱动的装载因为是在内核中进行,所以可能导致系统的崩溃

应用程序和内核模块的不同:

1.应用程序:除了多线程,大多数是从头到尾执行单个任务,退出时可以不管资源的释放或其他清除工作

2.内核模块:预先注册,以便服务于将来的某个请求,然后它的初始化函数就结束。好像告诉“我在这里,并且我能做这些工作”以及“我要离开啦,不要再让我做任何事情了”,并且模块的退出函数必须要释放初始化函数做的东西。

3.模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间中。

4.通常来讲,一个驱动程序要执行两类任务:模块中的某些函数作为系统调用的一部分而执行,而其他函数则负责中断处理

5.内核中的并发:对于内核代码,要时刻记住,同一时刻,有可能有许多事情正在发生,比如多个进程同时使用驱动程序,不一定是代码从头执行到尾。

6.应用程序在虚拟内存中布局,并具有一块很大的栈空间,而内核具有非常小的栈,它只能和一个4096字节大小的页那样小。所以,如果需要大的结构,则应该在调用时动态分配。

1..常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名
通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 谨慎使用,否则后果自负."

2.内核代码不能做浮点算术

3.lsmod通过读取/proc/modules虚拟文件来获得当前装载到内核的所有模块信息。也可以在sysfs虚拟文件系统的/sys/module下找到

4.驱动的入口函数,如果前面加_ _init,是对内核的暗示,表明该函数初始化后就丢掉,不占用内存

5.内核经常使用goto来处理错误

错误恢复有时用 goto 语句处理是最好的. 我们通常不愿使用 goto, 但是在我们的观念里,
这是一个它有用的地方. 在错误情形下小心使用 goto 可以去掉大量的复杂, 过度对齐的,
"结构形" 的逻辑. 因此, 在内核里, goto 是处理错误经常用到, 如这里显示的.
下面例子代码( 使用设施注册和注销函数)在初始化在任何点失败时做得正确:
int __init my_init_function(void)
{
int err;
err = register_this(ptr1, "skull"); /* registration takes a pointer and a name */
if (err)
goto fail_this;
err = register_that(ptr2, "skull");
if (err)
goto fail_that;
err = register_those(ptr3, "skull");
if (err)
goto fail_those;
return 0; /* success */
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_this(ptr1, "skull");
fail_this:
return err; /* propagate the error */

}

my_init_function 的返回值, err, 是一个错误码. 在 Linux 内核里, 错误码是负数, 属
于定义于 <linux/errno.h> 的集合. 如果你需要产生你自己的错误码代替你从其他函数得
到的返回值, 你应当包含 <linux/errno.h> 以便使用符号式的返回值, 例如 -ENODEV, -
ENOMEM, 等等. 返回适当的错误码总是一个好做法, 因为用户程序能够把它们转变为有意
义的字串, 使用 perror 或者类似的方法.
 

6.在搞Linux驱动移植/开发的时候,对于编译出来的驱动可以选择手动insmod,但是感觉很土:1. 需要指定路径; 2. 如果碰到存在依赖的,就丑陋不堪了。

但是modprobe可以很优雅的解决:直接$ modprobe XX_DRIVER_XX即可。

7.有时候相比较内核空间的驱动程序,编写用户空间程序来直接对设备端口进行读写容易多,但是仍有不足。

第三章:字符驱动

1.scull 是一个字符驱动, 操作一块内存区域好像它是一个设备。scull 的优势在于它不依赖硬件. scull 只是操作一些从内核分配的内存. 任何人都可以编译和运行scull, 并且 scull 在 Linux 运行的体系结构中可移植. 另一方面, 这个设备除了演示内核和字符驱
动的接口和允许用户运行一些测试之外, 不做任何有用的事情

2.获得一个 dev_t 的主或者次设备编号

MAJOR(dev_t dev);
MINOR(dev_t dev);

相反, 如果你有主次编号, 需要将其转换为一个 dev_t, 使用:
MKDEV(int major, int minor);
 

3.ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的参数个数如下:int ioctl(int fd, int cmd, …);其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就能在用户程序中使用ioctl函数控制设备的I/O通道。

文件结构struct file:

struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构. 注意 file
与用户空间程序的 FILE 指针没有任何关系. 一个 FILE 定义在 C 库中, 从不出现在内核
代码中. 一个 struct file, 另一方面, 是一个内核结构, 从不出现在用户程序中.
 

在内核源码中, struct file 的指针常常称为 file 或者 filp("file pointer"). 我们将
一直称这个指针为 filp 以避免和结构自身混淆. 因此, file 指的是结构, 而 filp 是结
构指针.
 

struct file {

mode_t f_mode;文件模式确定文件是可读的或者是可写的(或者都是)

loff_t f_pos;当前读写位置

unsigned int f_flags;这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查
O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用f_mode 而不是 f_flags
 

struct file_operations *f_op;和文件关联的操作

void *private_data;open 系统调用设置这个指针为 NULL

struct dentry *f_dentry;关联到文件的目录入口( dentry )结构
/* .......*/

};

inode 结构

inode 结构由内核在内部用来表示文件

inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于
编写驱动代码有用:
dev_t i_rdev;
    对于代表设备文件的节点, 这个成员包含实际的设备编号

struct cdev *i_cdev;
    struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这
个结构, 当节点指的是一个字符设备文件时.
 

读和写

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
 

读写代码需要拷贝一整段数据到或者从用户地址空间. 这个能力由下列内核函
数提供, 它们拷贝一个任意的字节数组, 并且位于大部分读写实现的核心中.
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count); 从内核拷贝到应用层
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);从应用层拷贝到内核层

scull 的内存使用
 

在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申请页。

void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
 

发布了114 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_40535588/article/details/90736363