Linux 设备驱动程序(一)(上)

简介: Linux 设备驱动程序(一)

前言

  本文主要用来摘录《Linux 设备驱动程序第三版》一书中学习知识点,本书基于 Linux 2.6.10 版本,源代码摘录基于 Linux 2.6.34 ,两者之间可能有些出入。

驱动模块构建

【视频】 Linux之驱动开发篇

Linux内核模块分析(module_init宏)

Linux驱动编程中EXPORT_SYMBOL() 介绍

Linux EXPORT_SYMBOL宏详解


一、设备驱动程序简介

二、构造和运行模块

1、vscode 编写

vscode软件设置头文件路径的方法

.vscode/c_cpp_properties.json 中文件如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/usr/src/linux-headers-4.15.0-142/include/",
                "/usr/src/linux-headers-4.15.0-142/arch/x86/include",
                "/usr/src/linux-headers-4.15.0-142/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

内核头文件路径为:

/lib/modules/`uname -r`/
# 其值为: /usr/src/linux-headers-4.15.0-142/

2、hello 模块

// hello.c
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void) {
    printk(KERN_DEBUG "Hello, world\n");
    return 0;
}

static void hello_exit(void) {
    printk(KERN_DEBUG "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);
# Makefile
obj-m := hello.o              # 要生成的模块名      
# hello-objs:= a.o b.o          # 生成这个模块名所需要的目标文件

KDIR := /lib/modules/`uname -r`/build
# KDIR := /home/liuqz/learnLinux/linux-4.15.18
PWD := $(shell pwd)

default:
  make -C $(KDIR) M=$(PWD) modules

clean:
  rm -rf *.o *.o.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
  rm -rf .cache.mk .*ko.cmd .*.mod.o.cmd .*.o.cmd

3、模块参数

static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);


内核支持的模块参数类型如下:

  • bool
  • invbool
    布尔值(取 truefalse),关联的变量应该是 int 型。invbool 类型反转其值,也就是说,true 值变成 false,而 false 变成 true
  • charp
    字符指针值。内核会为用户提供的字符串分配内存,并相应设置指针。
  • int
  • long
  • short
  • uint
  • ulong
  • ushort
    具有不同长度的基本整数值。以 u 开头的类型用于无符号值。

  模块装载器也支持数组参数,在提供数组值时用逗号划分各数组成员。要声明数组参数,需要使用下面的宏:

module_param_array(name,type,num, perm);

 其中,name 是数组的名称(也就是参数的名称),type 是数组元素的类型,num 是一个整数变量,而 perm 是常见的访问许可值。如果在装载时设置数组参数,则 num 会被设置为用户提供的值的个数。模块装载器会拒绝接受超过数组大小的值。


 如果我们需要的类型不在上面所列出的清单中,模块代码中的钩子可让我们来定义这些类型。具体的细节请参阅 moduleparam.h 文件。所有的模块参数都应该给定一个默认值;


 insmod 只会在用户明确设置了参数的值的情况下才会改变参数的值。模块可以根据默认值来判断是否是一个显式指定的参数。


 module_param 中的最后一个成员是访问许可值,我们应使用  中存在的定义。这个值用来控制谁能够访问 sysfs 中对模块参数的表述。如果 perm 被设置为 0 ,就不会有对应的 sysfs 入口项;否则,模块参数会在 /sys/module(注 3)中出现,并设置为给定的访问许可。如果对参数使用 S_IRUGO ,则任何人均可读取该参数,但不能修改; S_IRUGO|S_IWUSR 允许 root 用户修改该参数。注意、如果一个参数通过sysfs 而被修改,则如同模块修改了这个参数的值一样,但是内核不会以任何方式通知模块。大多数情况下,我们不应该让模块参数是可写的,除非我们打算检测这种修改并作出相应的动作。

  加载 hello 模块后,在 /sys/module 下有如下文件:

ls /sys/module/hello/ -l
total 0
-r--r--r-- 1 root root 4096 5月   8 14:48 coresize
drwxr-xr-x 2 root root    0 5月   8 14:48 holders
-r--r--r-- 1 root root 4096 5月   8 14:48 initsize
-r--r--r-- 1 root root 4096 5月   8 14:48 initstate
drwxr-xr-x 2 root root    0 5月   8 14:48 notes
-r--r--r-- 1 root root 4096 5月   8 14:48 refcnt
drwxr-xr-x 2 root root    0 5月   8 14:48 sections
-r--r--r-- 1 root root 4096 5月   8 14:48 srcversion
-r--r--r-- 1 root root 4096 5月   8 14:48 taint
--w------- 1 root root 4096 5月   8 14:48 uevent

4、在用户空间编写驱动程序

  首次接触内核的 Unix 程序员可能对编写模块比较紧张,然而编写用户空间程序来直接对设备端口进行读写就容易多了。

 相对于内核空间编程,用户空间编程具有自己的一些优点。有时候编写一个所谓的用户空间驱动程序是替代内核空间驱动程序的一个不错的方法。在这一小节,我们将讨论编写用户空间驱动程序的几个理由。但本书主要讲述内核空间的驱动程序,因此除了这里的讨论之外,我们不会进一步深入讨论这个话题。


 用户空间驱动程序的优点可以归纳如下:


可以和整个 C 库链接。驱动程序不用借助外部程序(即前面提到的和驱动程序一起发行的用于提供策略的用户程序)就可以完成许多非常规任务。


可以使用通常的调试器调试驱动程序代码,而不用费力地调试正在运行的内核。


如果用户空间驱动程序被挂起,则简单地杀掉它就行了。驱动程序带来的问题不会挂起整个系统,除非所驱动的硬件已经发生严重故障。


和内核内存不同,用户内存可以换出。如果驱动程序很大但是不经常使用,则除了正在使用的情况之外,不会占用太多内存。


良好设计的驱动程序仍然支持对设备的并发访问。


如果读者必须编写封闭源码的驱动程序,则用户空间驱动程序可更加容易地避免因为修改内核接口而导致的不明确的许可问题。


 例如,USB 驱动程序可在用户空间编写;具体可参阅 libusb 项目(libusb.sourceforge.net ,该项目还比较"年轻"),以及内核源代码中的 “gadgetfs” 。X 服务器是用户空间驱动程序的另一个例子。它十分清楚硬件可以做什么、不可以做什么,并且为所有的 X 客户提供图形资源。然而,值得注意的是目前基于帧缓冲区(frame-buffer)的图形环境正在慢慢成为发展趋势。这种环境下对于实际的图形操作,X 服务器仅仅是一个基于真正内核空间驱动程序的服务器。


 通常,用户空间的驱动程序被实现为一个服务器进程,其任务是替代内核作为硬件控制的唯一代理。客户应用程序可连接到该服务器并和设备执行实际的通信;这样,好的驱动程序进程可允许对设备的并发访问。其实这就是 X 服务器的本质。


 除了具备上述优点外,用户空间驱动程序也有很多缺点,下面列出其中最重要的几点:


中断在用户空间中不可用。对该限制,在某些平台上也有相应的解决办法,比如 IA32 架构上的 vm86 系统调用。

只有通过 mmap 映射 /dev/mem 才能直接访问内存,但只有特权用户才可以执行这个操作。

只有在调用 ioperm 或 iopl 后才可以访问 I/O 端口。然而并不是所有平台都支持这两个系统调用,并且访问 /dev/port 可能非常慢,因而并非十分有效。同样只有特权用户才能引用这些系统调用和访问设备文件。

响应时间很慢。这是因为在客户端和硬件之间传递数据和动作需要上下文切换。

更严重的是,如果驱动程序被换出到磁盘,响应时间将令人难以忍受。使用 mlock 系统调用或许可以缓解这一问题,但由于用户空间程序一般需要链接多个库,因此通常需要占用多个内存页。同样,mlock 也只有特权用户才能引用。

用户空间中不能处理一些非常重要的设备,包括(但不限于)网络接口和块设备等。

 如上所述,我们看到用户空间驱动程序毕竞做不了太多的工作。然而依然存在一些有意义的应用,例如对 SCSI 扫描设备(由包 SANE 实现)和 CD 刻录设备(由 cdrecord 和其他工具实现)的支持。这两种情况下,用户空间驱动动程序都依赖内核空间驱动程序 “SCSI generic”,它导出底层通用的 SCSI 功能到用户空间程序,然后再由用户空间驱动程序驱动自己的硬件。


 有一种情况适合在用户空间处理,这就是当我们准备处理一种新的、不常见的硬件时。在用户空间中我们可以研究如何管理这个硬件而不用担心挂起整个系统。一旦完成,就很容易将户空间驱动程序封装到内核模块中。

5、快速参考

  本节将总结本章中提到的内核函数、变量、宏以及 /proc 文件,可以作为对这些内容的一个参考。每一项都会在相关头文件之后列出。从本章开始,以后每一章里都会有类似的一节来总结引入的新符号。本节中出现的条目会以它们在文中出现的顺序列出:

insmod

modprobe

rmmod

  用来装载模块到正运行的内核和移除模块的用户空间工具。

#include<linux/init.h>
module_init(init_function);
module_exit(cleanup_function);

// 用于指定模块的初始化和清除函数的宏。

__init

__initdata

__exit

__exitdata

  仅用于模块初始化或清除阶段的函数(__init和__exit)和数据(__initdata 和__exitdata)标记。标记为初始化的项目会在初始化结束后丢弃;而退出项目在内核未被配置为可卸载模块的情况下被丢弃。内核通过将相应的目标对象放置在可执行文件的特殊 ELF 段中而让这些标记起作用。

#include <linux/sched.h>

// 最重要的头文件之一。该文件包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。

current->pid

current->comm

 当前进程的进程 ID 和命令名。

 

struct task_struct *current;

 当前进程。

 

obj-m

 由内核构造系统使用的 makefile 符号,用来确定在当前目录中应构造哪些模块。

 

/sys/module

/proc/modules

 /sys/module 是 sysfs 目录层次结构中包含当前已装载模块信息的目录。/proc/modules 是早期用法,

 只在单个文件中包括这些信息,其中包含了模块名称、每个模块使用的内存总量以及使用计数等。

 每一行之后还追加有额外的字符串,用来指定模块的当前活动标志。

 

vermagic.o

 内核源代码目录中的一个目标文件,它描述了模块的构造环境。

#include <linux/module.h>
// 必需的头文件,它必须包含在模块源代码中。

#include <linux/version.h>
// 包含所构造内核版本信息的头文件。

LINUX_VERSION_CODE

 整数宏,在处理版本依赖的预处理条件语句中非常有用。

 

EXPORT_SYMBOL (symbol);

EXPORT_SYMBOL_GPL (symbol);

 用来导出单个符号到内核的宏。第二个宏将导出符号的使用限于 GPL 许可证下的模块。

 

MODULE_AUTHOR (author);

MODULE_DESCRIPTION (description);

MODULE_VERSION(version_string);

MODULE_DEVICE_TABLE(table_info);

MODULE_ALIAS(alternate_name);

 在目标文件中添加关于模块的文档信息。

module_init(init_function);
module_exit(exit_function);
// 用来声明模块初始化和清除函数的宏。

#include <linux/moduleparam.h>
module_param(variable, type, perm);
// 用来创建模块参数的宏,用户可在装载模块时(或者对内建代码引导时)调整这些参数的值。
// 其中的类型可以是bool、charp、int、invbool、long、short、ushort、uint、ulong 或者 intarray.

#include <linux/kernel.h>
int printk(const char * fmt, ...);
// 函数 printf 的内核代码。

三、字符设备驱动程序

1、 主设备号和次设备号

(1)分配和释放设备编号

  在建立一个字符设备之前,我们的驱动程序首先要做的事情就是获得一个或者多个设备编号。完成该工作的必要函数是 register_chrdev_region,该函数在 中声明:

int register_chrdev_region(dev_t first, unsigned count, const char *name)

 其中,first 是要分配的设备编号范围的起始值。first 的次设备号经常被置为 0,但对该函数来讲并不是必需的。count 是所请求的连续设备编号的个数。注意,如果 count 非常大,则所请求的范围可能和下一个主设备号重叠,但只要我们所请求的编号范围是可用的,则不会带来任何问题。最后,name 是和该编号范围关联的设备名称,它将出现在 /proc/devices 和 sysfs 中。


 和大部分内核函数一样,register_chrdev_region 的返回值在分配成功时为 0。在错误情况下,将返回一个负的错误码,并且不能使用所请求的编号区域。


 如果我们提前明确知道所需要的设备编号,则 register_chrdev_region 会工作得很好。但是,我们经常不知道设备将要使用哪些主设备号;因此,Linux 内核开发社区一直在努力转向设备编号的动态分配。在运行过程中使用下面的函数,内核将会为我们恰当分配所需要的主设备号:

int alloc_chrdev_region(dev_t *dev, unsigned firstminor, unsigned count,
      const char *name)

  在上面这个函数中,dev 是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor 应该是要使用的被请求的第一个次设备号,它通常是 0countname 参数与 register_chrdev_region 函数是一样的。

  不论采用哪种方法分配设备编号,都应该在不再使用它们时释放这些设备编号。设备编号的释放需要使用下面的函数:

void unregister_chrdev_region(dev_t first, unsigned int count);

 通常,我们在模块的清除函数中调用 unregister_chrdev_region 函数。


 上面的函数为驱动程序的使用分配设备编号,但是它们并没有告诉内核关于拿来这些编号要做什么工作。在用户空间程序可访问上述设备编号之前,驱动程序需要将设备编号和内部函数连接起来,这些内部函数用来实现设备的操作。我们会马上讨论如何实现这种连接,但在此之前还需要进一步讨论有关设备号的内容。

(2)scull_load 脚本

#!/bin/sh
module="scull"
device="scull"
mode="664"

# 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定模块位置,
# 这是因为新的modutils默认不会在当前目录中查找模块。
/sbin/insmod ./$module.ko $* || exit 1

# 删除原有节点
rm -f /dev/${device}[0-3]

major=$(awk "\$2= =\"$module\"{print \$1}" /proc/devices)
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

# 给定适当的组属性及许可,并修改属组。
# 并非所有的发行版都具有 staff 组, 有些有 wheel 组。
group="staff"
grep -q '^staff:' /etc/group || group="wheel"

chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]

2、 一些重要的数据结构

(1)文件操作

// include/linux/fs.h
/*
 * NOTE:
 * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
 * can be called without the big kernel lock held in all filesystems.
 */
struct file_operations {
  /* 
   * 第一个 file_operations 字段并不是一个操作;相反,它是指向"拥有"该结
   * 构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模
   * 块。几乎在所有的情况下,该成员都会被初始化为 THIS_MODULE,它是定义在
   * <linux/module.h> 中的一个宏。
  */
  struct module *owner;
  /* 
   * 方法 llseek 用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回。
   * 参数 loff_t 一个"长偏移量",即使在 32 位平台上也至少占用 64 位的数据宽度。
   * 出错时返回一个负的返回值。如果这个函数指针是NULL,对seek的调用将会以某
   * 种不可预期的方式修改 file结构(在"file 结构"一节中有描述)中的位置计数器.
  */
  loff_t (*llseek) (struct file *, loff_t, int);
  /* 
   * 用来从设备中读取数据。该函数指针被赋为NULL值时,将导致read 系统调用出错
   * 并返回 -EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读
   * 取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数
   * 类型)。
  */
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  /* 
   * 向设备发送数据。如果没有这个函数,write系统调用会向程序返回一个-EINVAL。
   * 如果返回值非负,则表示成功写入的字节数。
  */
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  /* 
   * 初始化一个异步的读取操作--- 即在函数返回之前可能不会完成的读取操作。如
   * 果该方法为 NULL,所有的操作将通过 read(同步)处理。
  */
  ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  /* 
   * 初始化设备上的异步写入操作。
  */
  ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  /* 
   * 对于设备文件来说,这个字段应该为NULL。它仅用干读取目录,只对文件系统有用。
  */
  int (*readdir) (struct file *, void *, filldir_t);
  /* 
   * poll方法是 poll、epoll和 select 这三个系统调用的后端实现。这三个系统调用可用
   * 来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll方法应该返回一
   * 个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进
   * 程置于休眠状态直到 I/O 变为可能时的信息。如果驱动程序将 poll 方法定义为
   * NULL,则设备会被认为既可读也可写,并且不会被阻塞。
  */
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  /* 
   * 系统调用ioctl提供了一种执行设备特定命令的方法(如格式化软盘的某个磁道,这
   * 既不是读操作也不是写操作)。另外,内核还能识别一部分ioctl命令,而不必调用
   * fops表中的ioctl。如果设备不提供ioctl入口点,则对于任何内核未预先定义的请
   * 求,ioctl系统调用将返回错误(-ENOTTY,"No such ioctl for device,该设备无
   * 此 ioctl 命令")。
  */
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
  /* 
   * 
  */
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  /* 
   * 
  */
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  /* 
   * mmap用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那
   * 么mmap系统调用将返回 -ENODEV。
  */
  int (*mmap) (struct file *, struct vm_area_struct *);
  /* 
   * 尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明
   * 一个相应的方法。如果这个入口为NULL,设备的打开操作永远成功,但系统不会
   * 通知驱动程序。
  */
  int (*open) (struct inode *, struct file *);
  /* 
   * 对 flush 操作的调用发生在进程关闭设备文件描述符副本的时候,它应该执行(并
   * 等待)设备上尚未完结的操作。请不要将它同用户程序使用的 fsync 操作相混淆。
   * 目前,flush 仅仅用于少数几个驱动程序,比如,SCSI 磁带驱动程序用它来确保设
   * 备被关闭之前所有的数据都被写入到磁带中。如果 flush 被置为 NULL,内核将简单
   * 地忽略用户应用程序的请求。
  */
  int (*flush) (struct file *, fl_owner_t id);
  /* 
   * 当 file 结构被释放时,将调用这个操作。与 open相仿,也可以将 release 设置为
   * NULL(注 5)。
   *  
   * 注 5: 注意,release 并不是在进程每次调用 close 时都会被调用。只要 file 结构被共享(如
   * 在fork 或dup调用之后),release 就会等到所有的副本都关闭之后才会得到调用。如果
   * 需要在关闭任意一个副本时刷新那些特处理的数据,则应实现flush 方法。
  */
  int (*release) (struct inode *, struct file *);
  /* 
   * 该方法是fsync 系统调用的后端实现,用户调用它来刷新待处理的数据。如果驱动
   * 程序没有实现这一方法,fsync 系统调用返回 -EINVAL。
  */
  int (*fsync) (struct file *, struct dentry *, int datasync);
  /* 
   * 这是 fsync 方法的异步版本。
  */
  int (*aio_fsync) (struct kiocb *, int datasync);
  /* 
   * 这个操作用来通知设备其FASYNC 标志发生了变化。异步通知是比较高级的话题,
   * 将在第六章介绍。如果设备不支持异步通知,该字段可以是 NULL。
  */
  int (*fasync) (int, struct file *, int);
  /* 
   * lock方法用于实现文件锁定,锁定是常规文件不可缺少的特性、但设备驱动程序几
   * 乎从来不会实现这个方法。
  */
  int (*lock) (struct file *, int, struct file_lock *);
  /* 
   * sendpage是sendfile系统调用的另外一半,它由内核调用以将数据发送到对应的文
   * 件,每次一个数据页。设备驱动程序通常也不需要实现 sendpage。
  */
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  /* 
   * 该方法的目的是在进程的地址空间中找到一个合适的位置,以便将底层设备中的内
   * 存段映射到该位置。该任务通常由内存管理代码完成,但该方法的存在可允许驱动
   * 程序强制满足特定设备需要的任何对齐需求。大部分驱动程序可设置该方法为 NULL。
  */
  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  /* 
   * 该方法允许模块检查传递给 fcntl(F_SETFL...)调用的标志。
  */
  int (*check_flags)(int);
  /* 
   * 
  */
  int (*flock) (struct file *, int, struct file_lock *);
  /* 
   * 
  */
  ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
  /* 
   * 
  */
  ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
  /* 
   * 
  */
  int (*setlease)(struct file *, long, struct file_lock **);
};

补充:其他版本的实现

  /* 
   * 这些方法用来实现分散/聚集型的读写操作。应用程序有时需要进行涉及多个内存
   * 区域的单次读或写操作,利用上面这些系统调用可完成这类工作,而不必强加额外
   * 的数据拷贝操作。如果这些函数指针被设置为 NULL,就会调用 read 和 write 方法
   * (可能是多次)。
  */
  ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
  ssize_t (*writev) (struct file *,const struct iovec *, unsigned lang, loff_t *);
  /* 
   * 这个方法实现 sendfile 系统调用的读取部分。sendfile 系统调用以最小的复制操作
   * 将数据从一个文件描述符移动到另一个。例如,Web服务器可以利用这个方法将某
   * 个文件的内容发送到网络连接。设备驱动程序通常将 sendfile 设置为 NULL。
  */
  ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
  /* 
   * 当应用程序使用fcntl来请求目录改变通知时,该方法将被调用。该方法仅对文件
   * 系统有用、驱动程序不必实现 dir_notify。
  */
  int (*dir_notify)(struct file *, unsigned long);

(2)file 结构

 在  中定义的 struct file 是设备驱动程序所使用的第二个最重要的数据结构,注意、file 结构与用户空间程序中的 FILE 没有任何关联。FILE 在 C 库中定义且不会出现在内核代码中;而 struct file 是一个内核结构,它不会出现在用户程序中。


 file 结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的 file 结构)。它由内核在 open 时创建,并传递给在该文件上进行操作的所有函数,直到最后的 close 函数。在文件的所有实例都被关闭之后,内核会释放这个数据结构。


 在内核源码中,指向 struct file 的指针通常被称为 file 或 filp(“文件指针”)。为了不至于和这个结构本身相混淆,我们一致将该指针称为 filp。这样,file 指的是结构本身,filp 则是指向该结构的指针。


 struct file 中最重要的成员罗列如下。与上节相似,这张清单在首次阅读时可以略过。在下一节中将看到一些真正的 C 代码,我们会详细讨论其中的某些字段。

// include/linux/fs.h
struct file {
  /*
   * fu_list becomes invalid after file_free is called and queued via
   * fu_rcuhead for RCU freeing
   */
  union {
    struct list_head  fu_list;
    struct rcu_head   fu_rcuhead;
  } f_u;
  struct path   f_path;
#define f_dentry  f_path.dentry
#define f_vfsmnt  f_path.mnt
  /*
   * 与文件相关的操作。内核在执行 open操作时对这个指针赋值,以后需要处理这些
   * 操作时就读取这个指针。filp->f_op中的值决不会为方便引用而保存起来;也
   * 就是说,我们可以在任何需要的时候修改文件的关联操作,在返回给调用者之后,
   * 新的操作方法就会立即生效。例如,对应于主设备号1(/dev/null、/dev/zero等等)
   * 的open代码根据要打开的次设备号替换filp->f_op中的操作。这种技巧允许相
   * 同主设备号下的设备实现多种操作行为,而不会增加系统调用的负担。这种替换文
   * 件操作的能力在面向对象编程技术中称为"方法重载"。
  */
  const struct file_operations  *f_op;
  spinlock_t    f_lock;  /* f_ep_links, f_flags, no IRQ */
  atomic_long_t   f_count;
  /*
   * 文件标志,如 O_RDONLY、O_NONBLOCK和O_SYNC。为了检查用户请求的是否是非
   * 阻塞式的操作(我们将在第六章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O),
   * 驱动程序需要检查 O_NONBLOCK标志,而其他标志很少用到。注意,检查读 / 写权
   * 限应该查看 f_mode而不是 f_flags。所有这些标志都定义在<linux/fcntl.h>中。
  */
  unsigned int    f_flags;
  /*
   * 文件模式。它通过 FMODE_READ 和 FMODE_WRITE 位来标识文件是否可读或可写(或可读写)。
   * 读者可能会认为要在自己的 open 或 ioctl函数中查看这个字段,以便检
   * 查是否拥有读 / 写访问权限,但由于内核在调用驱动程序的 read 和 write 前已经检
   * 查了访问权限,所以不必为这两个方法检查权限。在没有获得对应访问权限而打开
   * 文件的情况下,对文件的读写操作将被内核拒绝,驱动程序无需为此而作额外的判断。
  */
  fmode_t     f_mode;
  /*
   * 当前的读/写位置。loff_t是一个 64位的数(用gcc的术语说就是 long long)。
   * 如果驱动程序需要知道文件中的当前位置,可以读取这个值,但不要去修改它。
   * read/write 会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对
   * filp->f_pos 进行操作。这一规则的一个例外是 llseek方法,该方法的目的本身
   * 就是为了修改文件位置。
  */  
  loff_t      f_pos;
  struct fown_struct  f_owner;
  const struct cred *f_cred;
  struct file_ra_state  f_ra;

  u64     f_version;
#ifdef CONFIG_SECURITY
  void      *f_security;
#endif
  /* needed for tty driver, and maybe others */
  /*
   * open 系统调用在调用驱动程序的 open 方法前将这个指针置为 NULL。驱动程序可
   * 以将这个字段用于任何目的或者忽略这个字段。驱动程序可以用这个字段指向已分
   * 配的数据,但是一定要在内核销毁 file 结构前在 release 方法中释放内存。
   * private_data是跨系统调用时保存状态信息的非常有用的资源,我们的大部分示
   * 例都使用了它。
  */
  void      *private_data;

#ifdef CONFIG_EPOLL
  /* Used by fs/eventpoll.c to link all the hooks to this file */
  struct list_head  f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
  struct address_space  *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
  unsigned long f_mnt_write_state;
#endif
};

更多:

  /*
   * 文件对应的目录项(dentry)结构。除了用filp->f_dentry->d_inode的方式来
   * 访问索引节点结构之外,设备驱动程序的开发者们一般无需关心 dentry 结构。
   * 实际的结构里还有其他一些字段,但它们对于设备驱动程序并没有多大用处。由于驱动
   * 程序从不自己填写file 结构、而只是对别处创建的 file 结构进行访问、所以忽略这
   * 些字段是安全的。
  */
  struct dentry *f_dentry:

(3)inode 结构

  内核用 inode 结构在内部表示文件,因此它和 file 结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的 file 结构,但它们都指向单个 inode 结构。

  inode 结构中包含了大量有关文件的信息。作为常规,只有下面两个字段对编写驱动程

序代码有用:

struct inode {
  // ...
  
  /* 对表示设备文件的 inode 结构,该字段包含了真正的设备编号。 */
  dev_t     i_rdev;
  union {
    struct pipe_inode_info  *i_pipe;
    struct block_device *i_bdev;
    /*
     * struct cdev 是表示字符设备的内核的内部结构。当 inode 指向一个字符设备文
     * 件时,该字段包含了指向 struct cdev 结构的指针。
    */
    struct cdev   *i_cdev;
  };  
  
  // ...  
}

  i_rdev 的类型在 2.5 开发系列版本中发生了变化,这破坏了大量驱动程序代码的兼容性。为了鼓励编写可移植性更强的代码,内核开发者增加了两个新的宏,可用来从一个 inode 中获得主设备号和次设备号:

  unsigned int iminor(struct inode *inode);
  unsigned int imajor(struct inode *inode);

  为了防止因为类似的改变而出现问题,我们应该使用上述宏、而不是直接操作 i_rdev

3、 字符设备的注册

// include/linux/cdev.h
struct cdev {
  struct kobject kobj;
  struct module *owner;
  const struct file_operations *ops;
  struct list_head list;
  dev_t dev;
  unsigned int count;
};


  我们前面提到,内核内部使用 struct cdev 结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或者多个上述结构(注 6)。为此,我们的代码应包含 ,其中定义了这个结构以及与其相关的一些辅助函数。

  分配和初始化上述结构的方式有两种。如果读者打算在运行时获取一个独立的 cdev 结构,则应该如下编写代码:

  struct cdev *my_cdev = cdev_alloc();
  my_cdev->ops = &my_fops;

  这时,你可以将 cdev 结构嵌入到自己的设备特定结构中,scull 就是这样做的。这种情况下,我们需要用下面的代码来初始化已分配到的结构:

// fs/char_dev.c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
  memset(cdev, 0, sizeof *cdev);
  INIT_LIST_HEAD(&cdev->list);
  kobject_init(&cdev->kobj, &ktype_cdev_default);
  cdev->ops = fops;
}

  另外,还有一个 struct cdev 的字段需要初始化。和 file_operations 结构类似,struct cdev 也有一个所有者字段,应被设置为 THIS_MODULE

  在 cdev 结构设置好之后,最后的步骤是通过下面的调用告诉内核该结构的信息:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
  p->dev = dev;
  p->count = count;
  return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

 这里,p 是 cdev 结构,dev 是该设备对应的第一个设备编号,count 是应该和该设备关联的设备编号的数量。count 经常取 1,但是在某些情形下,会有多个设备编号对应于一个特定的设备。例如,考虑 SCSI 磁带驱动程序,它通过每个物理设备的多个次设备号来允许用户空间选择不同的操作模式(比如密度)。


 在使用 cdev_add 时,需要牢记重要的一点。首先,这个调用可能会失败。如果它返回一个负的错误码,则设备不会被添加到系统中。但这个调用几乎总会成功返回,此时,我们又面临另一个问题:只要 cdev_add 返回了,我们的设备就 “活” 了,它的操作就会被内核调用。因此,在驱动程序还没有完全准备好处理设备上的操作时,就不能调用 cdev_add 。

  要从系统中移除一个字符设备,做如下调用:

void cdev_del(struct cdev *p)
{
  cdev_unmap(p->dev, p->count);
  kobject_put(&p->kobj);
}

  要请楚的是,在将 cdev 结构传递到 cdev_del 函数之后,就不应再访问 cdev 结构了。

4、快速参考

  本章介绍了下列符号和头文件。file_operations 结构和 file 结构的字段清单并没有在这里给出。

#include

dev_t

  dev_t 是内核中用来表示设备编号的数据类型。

/* 这两个去从设备编号中抽取出主 / 次设备号。 */
int MAJOR(dev_t dev);
int MINOR(dev_t dev);

/* 这个宏由主 / 次设备号构造一个 dev_t 数据项。 */
dev_t MKDEV(unsigned int major, unsigned int minor);

/*
 * "文件系统"头文件,它是编写设备驱动程序必需的头文件,其中声明了许多重要
 * 的函数和数据结构。 
 */
#include <linux/fs.h>

/*
 * 提供给驱动程序用来分配和释放设备编号范围的函数。在期望的主设备号预先知道
 * 的情况下,应调用 register_chrdev_region;而对动态分配,使用 alloc_chrdev_region。
*/
int register_chrdev_region(dev_t first, unsigned int count, char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned
            int count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);

/*
 * 老的(2.6 之前的)字符设备注册例程。2.6 内核也提供了仿效该例程的函数,但是
 * 新代码不应该再使用该函数。如果主设备号不是0,则不加修改地使用;否则,系
 * 统将为该设备动态地分配编号。
*/
int register_chrdev(unsigned int major, const char *name, struct
          file_operations *fops);

/*
 * 用于注销由 register_chrdev 函数注册的驱动程序。major 和 name 字符串必须包
 * 含与注册该驱动程序时使用的相同的值。
*/
int unregister_chrdev(unsigned int major, const char *name);

struct file_operations;

struct file;

struct inode;

 大多数设备驱动程序都会用到的三个重要数据结构。file_operations 结构保存

 了字符驱动程序的方法; struct file 表示一个打开的文件,而 struct inode

 表示一个磁盘上的文件。

/* 用来管理 cdev 结构的函数,内核中使用该结构表示字符设备。 */
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *dev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);

/* 一个方便使用的宏,它可用于从包含在某个结构中的指针获得结构本身的指针。 */
#include <linux/kernel.h>
container_of(pointer, type, field);

/* 该头文件声明了在内核代码和用户空间之间移动数据的函数。 */
#include <asm/uaccess.h>

/* 在用户空间和内核空间之间拷贝数据。 */
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
unsigned long copy_to_user (void *to, const void *from, unsigned long count);

四、调试技术

可参考 ==> 内核调试方法

     十五、调试

1、 内核中的调试支持

 在第二章,我们建议读者构造并安装自己的内核,而不是运行发行版自带的原有内核。运行自己内核的一个最重要的原因之一是因为内核开发者已经在内核中建立了多项用于调试的功能。但这些功能会造成额外的输出,并导致性能下降,因此发行版厂商通常会禁止发行版内核中的这些功能。但是作为一名内核开发者,调试需求具有更高优先级,从而应该乐意接受因为额外的调试支持而导致的(最小)系统负载。


 这里,我们列出了用于内核开发的几个配置选项。除特别指出外,所有这些选项均出现在内核配置工具的 “kernel hacking” 菜单中。注意,并非所有体系架构都支持其中的某些选项。


CONFIG_DEBUG_KERNEL

该选项仅仅使得其他的调试选项可用。我们应该打开该选项,但它本身不会打开所有的调试功能。

CONFIG_DEBUG_SLAB

这是一个非常重要的选项,它打开内核内存分配函数中的多个类型的检查;打开该检查后,就可以检测许多内存溢出及忘记初始化的错误。在将已分配内存返回给调用者之前,内核将把其中的每个字节设置为 0xa5,而在释放后将其设置为 0x61。

如果读者在自己驱动程序的输出中,或者在 oops 信息中看到上述 “毒剂” 字符,则可以轻松判断问题所在。在打开该调试选项后,内核还会在每个已分配内存对象的前面和后面放置一些特殊的防护值;这样,当这些防护值发生变化时,内核就可以知道有些代码超出了内存的正常访问范围、并 “大声抱怨” 。同时,该选项还会检查更多隐蔽的错误。

CONFIG_DEBUG_PAGEALLOC

在释放时,全部内存页从内核地址空间中移出。该选项将大大降低运行速度,但可以快速定位特定的内存损坏错误的所在位置。

CONFIG_DEBUG_SPINLOCK

打开该选项项,内核将捕获对未初始化自旋锁的操作,也会捕获诸如两次解开同一锁的操作等其他错误。

CONFIG_DEBUG_SPINLOCK_SLEEP

该选项将检查拥有自旋锁时的休眠企图。实际上,如果调用可能引起休眠的函数,这个选项也会生效,即使该函数可能不会导致真正的休眠。

CONFIG_INIT_DEBUG

标记为 __init(或者 __initdata)的符号将会在系统初始化或者模块装载之后被丢弃。该选项可用来检查初始化完成之后对用于初始化的内存空间的访问企图。

CONFIG_DEBUG_INFO

该选项将使内核的构造包含完整的调试信息。如果读者打算利用 gdb 调试内核,将需要这些信息。如果计划使用 gdb,还应该打开 CONFIG_FRAME_POINTER 选项。

CONFIG_MAGIC_SYSRQ

打开 "SysRq 魔法(magic SysRq)"按键。我们将在本章后面的 “系统挂起” 一节中讲述该按键。

CONFIG_DEBUG_STACKOVERFLOW

CONFIG_DEBUG_STACK_USAGE

这些选项可帮助跟踪内核栈的溢出问题。栈溢出的确切信号是不包含任何合理的反向跟踪信息的 oops 清单。第一个选项将在内核中增加明确的溢出检查;而第二个选项将让内核监视栈的使用,并通过 SysRq 按键输出一些统计信息。

CONFIG_KALLSYMS

该选项出现在 “General setup/Standard features(一般设置 / 标准功能)” 菜单中,将在内核中包含符号信息;该选项默认是打开的。该符号信息用于调试上下文;没有此符号,oops 清单只能给出十六进制的内核反向跟踪信息,这通常没有多少用处。

CONFIG_IKCONFIG

CONFIG_IKCONFIG_PROC

这些选项出现在 “General setup(一般设置)” 菜单中,会让完整的内核配置状态包含到内核中,并可通过 /proc 访问。大多数内核开发者清楚地知道自己所使用的配置,因此并不需要这两个选项(会使得内核变大)。然而,如果读者要调试的内核是由其他人建立的,则上述选项会比较有用。

CONFIG_ACPI_DEBUG

该选项出现在 “Power management/ACPI(电源管理/ACPI)” 菜单中。该选项将打开 ACPI(Advanced Configuration and Power Interface,高级配置和电源接口)中的详细调试信息。如果怀疑自己所遇到的问题和 ACPI 相关,则可使用该选项。

CONFIG_DEBUG_DRIVER

在 “Device drivers(设备驱动程序)” 菜单中。该选项打开驱动程序核心中的调试信息,它可以帮助跟踪底层支持代码中的问题。本书第十四章将闻述驱动程序核心相关的内容。

CONFIG_SCSI_CONSTANTS

该选项出现在 “Device drivers/SCSI device support(设备驱动程序 /SCSI 设备支持)” 菜单中,它将打开详细的 SCSI 错误消息。如果读者要编写 SCSI 驱动程序,则可使用该选项。

CONFIG_INPUT_EVBUG

该选项可在 “Device drivers/Input device support(设备驱动程序/输入设备支持)” 中找到,它会打开对输入事件的详细记录。如果读者要针对输入设备编写驱动程序,则可使用该选项。注意该选项会导致的安全问题:它会记录你键入的任何东西,包括密码。

CONFIG_PROFILING

该选项可在 “Profiling support(剖析支持)” 菜单中找到。剖析通常用于系统性能的调节,但对跟踪内核挂起及相关问题也会有帮助。

 在我们讲解不同的内核问题跟踪方法时,将再次遇到上述选项。在此之前,先描述一下经典的调试技术:print 语句。

2、通过打印调试

  最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用 printf 显示监视信息。调试内核代码的时候,可以用 printk 来完成相同的工作。

3、 通过查询调试

(1)使用 /proc 文件系统

 所有使用 /proc 的模块必须包含 ,并通过这个头文件来定义正确的函数。

 为创建一个只读的 /proc 文件,驱动程序必须实现一个函数,用于在读取文件时生成数据。当某个进程读取这个文件时(使用 read 系统调用),读取请求会通过这个函数发送到驱动程序模块。我们把注册接口放到本节后面,先直接讲述这个函数。


 在某个进程读取我们的 /proc 文件时,内核会分配一个内存页(即 PAGE_SIZE 字节的内存块),驱动程序可以将数据通过这个内存页返回到用户空间。i 缓冲区会传入我们定义的函数,而该函数称为 read_proc 方法:

int (*read_proc)(char *page, char **start, off_t offset, int count,
        int *eof, void *data);

 参数表中的 page 指针指向用来写入数据的缓冲区;函数应使用 start 返回实际的数据写到内存页的哪个位置(对此后面还将进一步谈到);offset 和 count 这两个参数与 read 方法相同。eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data 参数是提供给驱动程序的专用数据指针,可用于内部记录。


 该函数必须返回存放到内存页缓冲区的字节数,这一点与 read 函数对其他类型文件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标志,而 start 的用法就有点复杂了,它可以帮助实现大(大于一个内存页)的 /proc 文件。


 start 参数的用法看起来有些特别,它用来指示要返回给用户的数据保存在内存页的什么位置。在我们的 read_proc 方法被调用时,*start 的初始值为 NULL。如果保留 *start 为空,内核将假定数据保存在内存页偏移量 0 的地方;也就是说,内核将对 read_proc 作如下简单假定:该函数将虚拟文件的整个内容放到了内存页,并同时忽略 offset 参数。相反,如果我们将 *start 设置为非空值,内核将认为由 *start 指向的数据是 offset 指定的偏移量处的数据,可直接返回给用户。通常,返回少量数据的简单 read_proc 方法可忽略 start 参数,复杂的 read_proc 方法会将 *start 设置为页面,并将所请求偏移量处的数据放到内存页中。


 长久以来,关于 /proc 文件还有另一个主要问题,这也是 start 意图解决的一个问题。有时,在连续的 read 调用之间,内核数据结构的 ASCII 表述会发生变化,以至于读取进程发现前后两次调用所获得的数据不一致。如果把 *start 设为一个小的整数值,那么调用程序可以利用它来增加 filp->f_pos 的值,而不依赖于返回的数据量,因此也就使 f_pos 成为 read_proc 过程的一个内部记录值。例如,如果 read_proc 函数从一个大的结构数组返回数据,并且这些结构的前五个已经在第一次调用中返回,那么可将 *start 设置为 5。下次调用中这个值将被作为偏移量;驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作 “hack” ,可以在 /fs/proc/generic.c 中看到。

五、并发和竞态

1、快速参考

  本章介绍了大量用来管理并发的符号,我们在这里总结了其中最重要的一些符号:

/* 定义信号量及其操作的包含文件。 */
#include <asm/semaphore.h>

/* 用于声明和初始化用在互斥模式中的信号量的两个宏。 */
DECLARE_MUTEX (name);
DECLARE_MUTEX_LOCKED(name);

/* 这两个函数可在运行时初始化信号量。 */
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);

/*
 * 锁定和解锁信号量。如果必要,down 会将调用进程置于不可中断的休眠状态;相
 * 反,down_interruptible 可被信号中断。down_trylock 不会休眠,并且会在信号量
 * 不可用时立即返回锁定信号量的代码最后必须使用 up 解锁该信号量。
*/
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);

/* 信号量的读取者 / 写入者版本以及用来初始化这种信号量的函数。 */
struct rw_semaphore;
init_rwsem(struct rw_semaphore *sem);

/* 获取并释放对读取者 / 写入者信号量的读取访问的函数。 */
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);

/* 对读取者 / 写入者信号量的写入访问进行管理的函数。 */
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)
void up_write(struct rw_semaphore *sem)
void downgrade_write(struct rw_semaphore *sem)



/*
 * 描述 Linux 的 completion 机制的包含文件,以及用于初始化 completion 的常用方
 * 法。INIT_COMPLETION 只能用于对已使用过的 completion 的重新初始化。
*/
#include <linux/completion.h>
DECLARE_COMPLETION(name);
init_completion(struct completion *c);
INIT_COMPLETION(struct completion c);

/* 等待一个 completion 事件的发生。 */
void wait_for_completion(struct completion *c);

/*
 * 发出completion事件信号。complete最多只能唤醒一个等待的线程,而complete_all
 * 会唤醒所有的等待者。
*/
void complete(struct completion *c);
void complete_all(struct completion *c);

/* 通过调用 complete 并调用当前线程的 exit 函数而发出 completion 事件信号。 */
void complete_and_exit(struct completion *c, long retval);


/* 定义自旋锁接口的包含文件,以及初始化自旋锁的两种方式。 */
#include <linux/spinlock.h>
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init(spinlock_t *lock);

/* 锁定自旋锁的不同方式,某些方法会禁止中断。 */
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);

/* 上述函数的非自旋版本。这些函数在无法获得自旋锁时返回零,否则返回非零。 */
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);

/* 释放自旋锁的相应途径。 */
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

/* 初始化读取者 / 写入者锁的两种方式。 */
rwlock_t lock = RW_LOCK_UNLOCKED
rwlock_init(rwlock_t *lock);

/* 获取对读取者 / 写入者锁的读取访问的函数。 */
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);

/* 释放对读取者 / 写入者自旋锁的读取访问的函数。 */
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

/* 获取对读取者 / 写入者自旋锁的写入访问的函数。 */
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);

/* 释放对读取者 / 写入者自旋锁的写入访问的函数。 */
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);


/* 整数变量的原子访问。对 atomic_t 变量的访问必须仅通过上述函数 */
#include <asm/atomic.h>
atomic_t v = ATOMIC_INIT(value);
void atomic_set(atomic_t *v, int i);
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
int atomic_add_negative(int i, atomic_t *v);
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);


/*
 * 对位置的原子访问,它们可用于标志或锁变量。使用这些函数可避免因为对相应位 
 * 的并发访问而导致的任何竞态
 */
#include <asm/bitops.h>
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
test_bit(nr, void *addr);
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);



/* 定义 seqlock 的包含文件,以及初始化 seqlock 的两种方式 */
#include <linux/seqlock.h>
seqlock_t lock = SEQLOCK_UNLOCKED;
seqlock_init(seqlock_t *lock);

/* 用于获取受 seqlock 保护的资源的读取访问的函数。 */
unsigned int read_seqbegin(seqLock_t *lock);
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry(seqlock_t *lock, unsigned int seq);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, 
               unsigned long flags);

/* 用于获取受 seqlock 保护资源的写入访问的函数。 */
void write_seqlock(seqlock_t *lock);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
int write_tryseqlock(seqlock_t *lock);

/* 用于释放受 seqlock 保护的资源的写入访问的函数。 */
void write_sequnlock(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigmed long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);

/* 使用读取 - 复制 - 更新(RCU)机制时需要的包含文件。 */
#include <linux/rcupdate.h>

/* 获取对受 RCU 保护的资源的读取访问的宏。 */
void rcu_read_lock;
void rcu_read_unlock;

/* 准备用于安全释放受RCU 保护的资源的回调函数,该函数将在所有的处理器被调度运行。 */
void call_rcu(struct rcu_head *head, void (*func) (void *arg), void *arg);

Linux 设备驱动程序(一)(中):https://developer.aliyun.com/article/1597396

目录
相关文章
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
18天前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
22 6
|
18天前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
23 5
|
18天前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
18 3
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
16 3
|
18天前
|
安全 数据管理 Linux
Linux 设备驱动程序(一)(中)
Linux 设备驱动程序(一)
17 2
|
18天前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
10 1
|
18天前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
13 1
|
18天前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
16 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1