Linux驱动开发学习笔记(更新中)

设备驱动程序,简称驱动程序。它是一个允许计算机软件与硬件交互的程序,设备驱动程序是一种可以使计算机与设备进行通信的特殊程序,可以说相当于硬件的接口。

设备驱动程序用来将硬件本身的功能告诉操作系统,完成硬件设备电子信号与操作系统及软件的高级编程语言之间的互相翻译。

设备驱动的分类:字符设备、块设备、网络设备

  1. 字符设备:指那些能一个字节一个字节读取数据的设备,一般需要在驱动层实现open()、close()、read()、write()、ioctl()等函数。内核为字符设备对应一个文件
  2. 块设备:与字符设备类似、一般是像磁盘一样的设备。在块设备中可以容纳文件系统,并存储大量信息。Linux可以让应用程序像访问字符设备一样访问块设备,一次只读取一个字节。所以块设备从本质上更像一个字符设备的扩展。但是块设备比字符设备要求更复杂的数据结构来描述,其内部的实现也是不一样的
  3. 网络设备:主要负责主机之间的数据交换。网络设备主要是面向数据包的接收和发送而设计的。网络设备实现了一种套接字接口,任何网络数据传输都可以通过套接字来完成。

模块是可以在运行时加入内核的代码,Linux内核支持多种模块,驱动程序就是其中最重要的一种。

每一个模块由编译好的目标代码组成,可以使用insmod命令将模块加入正在运行的内核,也可以使用rmmod命令将一个未使用的模块从内核中删除。不允许删除正在使用的模块。

模块在内核启动时装在被称为静态装载,在内核已经运行时装载被称为动态加载。模块可以扩充内核所期望的任何功能。但通常用于实现设备驱动程序。

在Linux上的程序开发一般分为两种:一种是内核及驱动程序开发,另一种是应用程序开发。这两种开发种类对应Linux的两种状态,分别是内核态和用户态。

如何将自己编写的内核代码融入Linux内核中。增加相应的Linux配置选项,并最终被编译进Linux内核。

arch目录 - 包含与体系结构相关的代码,每一种平台都有一种相应的目录:

drivers目录 - 包含了Linux内核支持的大部分驱动程序,每种驱动程序都占用一个子目录:

fs目录 - 包含了Linux所支持的所有文件系统相关的代码,每个子目录中包含一种文件系统

其他目录介绍:

自己构建嵌入式Linux操作系统首先需要对内核源代码进行相应的配置,这些配置决定了嵌入式Linux操作系统所支持的功能,编译程序通过配置文件配置系统。

Linux内核源代码编译机制:

  1. Makefile文件:根据配置的情况,构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成Linux内核二进制文件。
  2. Kconfig文件: 为用户提供一个层次化的配置选项集。make menuconfig命令通过分布在各个子目录中的Kconfig文件构建配置用户界面
  3. 配置文件(.config): 当用户配置完后,将配置信息保存在.config中

当执行menuconfig命令时,配置程序会依次从目录由浅入深查找每一个Kbuild文件,依照这个文件中的数据生成一个配置菜单。在配置菜单中根据需要配置完成后会在主目录下生成一个.config文件,此文件中保存了配置信息。

然后执行make命令时,会依赖生成的.config文件,以确定哪些功能将编译入内核中。然后递归地进入每一个目录,寻找Makefile文件,编译相应的代码。

Hello World驱动程序

驱动模块的组成:

  1. 头文件:

驱动模块会使用内核中的许多函数,所以需要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的。这两个头文件是:

  1. 模块参数:

模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。

  1. 模块加载函数和模块卸载函数:

模块加载/模块卸载时,需要执行的函数

  1. 模块许可声明:

表示模块受内核支持的程度。需要使用MODULE_LICENSE表示该模块的许可权限。

如果一个模块没有包含任何许可权,那么就会认为是不符合规范的。如果内核加载这种模块,会收到内核加载了一个非标准模块的警告。

Hello World驱动模块

复制代码
#include <linux/init.h>
#include <linux/module.h>

static int hello_init(void)
{
    printk(KERN_ALERT "Hello,World\n");
    return 0;
}
static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye,World\n");
    return 0;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

Makefile文件

复制代码
ifeq ($(KERNELRELEASE))
    KERNELDIR ?=/linux-2.6.29.4/linux-2.6.29.4
    PWD := $(shell pwd)
modules :
    S(MAKE)-C $(KERNELDIR)M-$(PWD)modules
modules install:
    S(MAKE)-C $(KERNELDIR)M=S(PwD)modules install
clean :
    rm -rf *.o*~ core depend *.cmd *,ko*.mod.c .tmp versions
else
    obj-m :- hello.o
endif

有了Makefile文件,就可以在模块所在目录下执行make命令,生成模块文件了。

模块操作:

insmod命令加载模块:

使用insmod hello.ko可以加载hello.ko模块。模块加载后会自动调用hello_init()函数

rmmod命令卸载模块:

如果模块没有被使用,那么执行rmmod hello.ko就可以卸载hello.ko模块

modprobe命令:

是比较高级的加载和删除模块命令,其可以解决模块之间的依赖性问题

lsmod命令:

列出已经加载的模块和信息。可以查询哪些模块已经被加载

modinfo命令:

用于查询模块的相关信息

模块参数:

用户空间的应用程序可以接受用户的参数,设备驱动程序有时候也需要接受参数。例如当一个模块可以实现两种相似功能,这时可以传递一个参数到驱动模块,以决定其使用哪一种功能。参数需要在加载模块时指定。

我们可以用"module_param(参数名,参数类型,参数读/写权限)"为模块定义一个参数

eg:

复制代码
module_param(num, int, S_IRUGO); 
module_param(book_name, charp, S_IRUGO); 

其中第三个字段:S_IRUGO = (S_IRUSR|S_IRGRP|S_IROTH)

S_IRUSR: 用户读

S_IRGRP:用户组读

S_IROTH:其他读

模块之间通信:

在模块1的驱动代码中通过导出宏EXPORT_SYMBOL导出到内核符号表(/proc/kallsyms文件,它用来记录符号以及符号所在的内存地址),让内核知道其定义的函数可以被其他函数使用。使用EXPORT_SYMBOL使函数变为导出函数是很方便的,但是不能随便使用,导出的函数不能出现相同的函数名。(编译器认为模块中的函数都是私有的,不同模块出现相同的函数名,并不会对编译产生影响)

eg:

add_sub.c

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include "add_sub.h"
long add_integer(int a,int b)
{
    return a+b;
}
long sub integer(int arint b)
{
    return a-b
}
EXPORT SYMBOL(add integer);
EXPORT SYMBOL(sub integer);
MODULE LICENSE("Dual BSD/GPL");

add_sub.h

复制代码
#ifndef _ADD_SUB_H_
#define _ADD_SUB_H_
long add integer(long a,long b);
long sub integer(long a,long b);
#endif

测试模块test

test 模块用来测试 add_sub模块提供的两个方法,同时test 模块也可以接收一个AddOrSub 参数,用来决定是调用 add integer()函数还是sub integer()函数。当 AddOrSub为1时,调用 add integer()函数;当AddOrSub不为1时,调用sub integer()函数。test 模块的代码如下:

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include "add_sub.h"     /*不要使用<>包含文件,否则找不到该文件*/

/*定义模块传递的参数a,b*/
static long a=1;
static long b=1;
static int AddorSub =1;
static int test init(void)/*模块加载函数*/
{
    long result=0;
    printk(KERN ALERT "test init\n");
    if(1==AddOrSub)
    {
        result=add integer(a,b);
    }
    else
    {
        result=sub integer(a,b);
    }
    printk(KERN_ALERT "The %s result is $ld",AddorSub==1?"Add":"Sub",result);
    return 0;
}

/*模块卸载函数*/
static void test exit(void)
{
    printk(KERN_ALERT "test exit\n");
}

module_init(test init);
module_exit(test exit);
module_param(a,long,S_IRUGO);
module_param(b,long,S_IRUGO);
module_param(AddOrSub,int,S_IRUGO);
/*描述信息*/
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Zheng Qiang");
MODULE_DESCRIPTION("A module for testing module params and EXPORT SYMBOL");
MODULE_VERSION("V1.0");

在加载test模块之前,需要先加载add_sub模块,test模块才能访问add_sub模块提供的导出函数

复制代码
insmod add_sub.ko

使用insmod加载模块,并传递参数到模块中:

复制代码
insmod test.ko a=3 b=2 AddOrSub=2 #参数之间不用逗号隔开

将模块静态编译进内核:

  1. 编写驱动程序文件
  2. 将驱动程序文件放到Linux内核源码的相应目录中,如果没有合适目录,可以自己建立一个目录存放驱动程序文件
  3. 在目录的Kconfig文件中添加新驱动程序对应的项目编译选项
  4. 在目录的Makefile文件中添加新驱动程序的编译语句

Kconfig文件---对应着内核的配置菜单,如果想添加新的驱动到内核的源码中,就需要修改Kconfig文件

Kconfig语法

Kconfig配置文件描述了一系列的菜单入口。除了帮助信息之外,每一行都以一个关键字开始,关键字如下:

复制代码
config
menuconfig
choice/endchoice
comment
menu/endmenu
if/endif

前五个关键字都定义了一个菜单选项,if/endif是一个条件选项。

config用来定义新的配置选项

menuconfig与config类似,它在config的基础上要求所有的子选项作为独立的行显示

menu和endmenu之间的菜单入口都会成为父菜单的子菜单

eg:

复制代码
menu "Network device support" //父菜单
    depends on NET  
config NETDEVICES  //子菜单
    ... 
endmenu 

Makefile

  1. 目标定义:

即用来定义哪些内容要作为模块编译,哪些要编译并连接进内核。

eg:

复制代码
obj-y += foo.o

表示要由foo.c或者foo.s文件编译得到foo.o并连接进内核,而obj-m则表示该文件要作为模块编译。

除了y、m以外的obj-x形式的目标都不会被编译

而更常见的作法是根据.config文件的CONFIG_变量来决定编译方式:

eg:

复制代码
obj-$(CONFIG_ISDN) += isdn.o
obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o

即对$后的这个变量取值,如果为y则是obj-y,如果为m则是obj-m

除了obj-形式的目标以外,还有lib-y library库,hostprogs-y主机程序等目标,但基本都应用在特定目录。

  1. 多文件模块定义

最简单的makefile文件如上面一句话的形式就够了,如果一个模块由多个文件组成,会稍微复制,这时候应该采用模块名加-y或-objs后缀的形式来定义模块的组成文件。

eg:

复制代码
obj-$(CONFIG_EXT2_FS) += ext2.o 
ext2-y := balloc.o dir.o file.o fsync.o ialloc.o inode.o \ 
        ioctl.o namei.o super.o symlink.o 
ext2-$(CONFIG_EXT2_FS_XATTR)     += xattr.o xattr_user.o xattr_trusted.o 
ext2-$(CONFIG_EXT2_FS_POSIX_ACL) += acl.o 
ext2-$(CONFIG_EXT2_FS_SECURITY)  += xattr_security.o 
ext2-$(CONFIG_EXT2_FS_XIP)   += xip.o 

模块名为ext2,它由balloc.o dir.o file.o fsync.o ialloc.o inode.o ioctl.o namei.o super.o symlink.o这些目标文件最终链接生成ext2.o直至ext2.ko文件。是否包括xattr.o acl.o等则取决与内核配置文件的配置情况,如果CONFIG_EXT2_FS_POSIX_ACL被选择,则编译acl.c得到acl.o并最终链接进ext2

  1. 目录层次的迭代

eg:

复制代码
obj-$(CONFIG_EXT2_FS) += ext2/

当 CONFIG_EXT2_FS 的值为y或m时,kbuild将会把 ext2目录 添加到构建目标列表中,确保目录及其子目录中的所有代码按需编译

  • 若值为 y:编译 ext2/ 目录下的代码并直接链接到内核镜像中。
  • 若值为 m:将 ext2/ 目录下的代码编译为可加载的内核模块(.ko 文件)。

Linux文件操作

Linux的文件草走系统调用涉及创建、打开、读写和关闭文件

  1. 创建

函数原型:

复制代码
int creat(const char *filename, mode_t mode); 

参数mode指定新建文件的存取权限,它同umask一起决定文件的最终权限(mode&umask)

  1. 打开

函数原型:

复制代码
int open(const char *pathname, int flags);  
int open(const char *pathname, int flags, mode_t mode);  

open()函数有两个形式,其中pathname是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下),flags是如下所示的一个值或是几个值的组合:

mode标志用来表示文件的访问权限:

除了通过上述宏进行"或"产生标志以外,也可以用数字表示:

Linux用5个数字来表示文件的各种权限:

第一位表示设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;最后一位表示其他人的权限。

每个数字可以取1(执行权限)、2(写权限)、4(读权限)、0(无)或者是这些值的和。

eg:

要创建一个用户可读、 可写、可执行,但是组没有权限,其他人可以读、可以执行的文件,并设置用户ID位。

那么,我们应该使用的模式是1(设置用户ID)、0(不设置组ID)、 7(1+2+4,读、写、执行)、0(没有权限)、5(1+4,读、执行)即10 705:

复制代码
open("test", O_CREAT, 10 705);

以O_CREAT为标志的open实际上实现了文件创建的功能

  1. 读写

在文件打开后,才可以进行读写操作,Linux中提供文件读写的系统调用是read、write函数

复制代码
int read(int fd, const void *buf, size_t length); 
int write(int fd, const void *buf, size_t length); 
  1. 定位

对于随机文件,我们可以随机地执行位置读写,使用定位函数进行偏移

复制代码
int lseek(int fd, offset_t offset, int whence);

lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的位置。

参数whence可使用下述值:

SEEK_SET:相对文件开头

SEEK_CUR:相对文件读写指针的当前位置

SEEK_END:相对文件末尾

offset可取负值,即将文件指针相对相反方向偏移

由于lseek函数的返回值是文件指针相对于文件头的位置,因此我们可以可以获得文件的长度信息:

复制代码
lseek(fd, 0, SEEK_END); 
  1. 关闭

当我们操作完成以后,我们就要关闭文件,只需要传入fd文件描述符,调用close就行

Linux文件系统与设备驱动

应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。

在设备驱动程序的设计中,一般而言,会关心file和inode这两个结构体。

  1. file结构体

结构体定义:

复制代码
 struct file 
  { 
    union { 
      struct list_head fu_list; 
      struct rcu_head fu_rcuhead; 
    } f_u; 
    struct dentry *f_dentry; /*与文件关联的目录入口(dentry)结构*/ 
    struct vfsmount *f_vfsmnt; 
    struct file_operations *f_op; /* 和文件关联的操作*/ 
   atomic_t f_count; 
   unsigned int f_flags;/*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/ 
   mode_t f_mode; /*文件读/写模式,FMODE_READ和FMODE_WRITE*/ 
   loff_t f_pos; /* 当前读写位置*/ 
   struct fown_struct f_owner; 
   unsigned int f_uid, f_gid; 
   struct file_ra_state f_ra; 
  
   unsigned long f_version; 
   void *f_security; 
  
   /* tty驱动需要,其他的也许需要 */ 
   void *private_data; /*文件私有数据*/ 
   ...  
   struct address_space *f_mapping; 
 }; 

文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data 在设备驱动中被广泛应用,大多被指向设备驱动自定义用于描述设备的结构体。

  1. inode结构体

VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信 息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。

结构体定义:

复制代码
  struct inode { 
      ... 
      umode_t i_mode; /* inode的权限 */ 
      uid_t i_uid; /* inode拥有者的id */ 
      gid_t i_gid; /* inode所属的群组id */ 
      dev_t i_rdev; /* 若是设备文件,此字段将记录设备的设备号 */ 
      loff_t i_size; /* inode所代表的文件大小 */ 
   
      struct timespec i_atime; /* inode最近一次的存取时间 */ 
     struct timespec i_mtime; /* inode最近一次的修改时间 */ 
     struct timespec i_ctime; /* inode的产生时间 */ 
  
     unsigned long i_blksize; /* inode在做I/O时的区块大小 */ 
     unsigned long i_blocks; /* inode所使用的block数,一个block为512 byte*/ 
  
     struct block_device *i_bdev;  
   /*若是块设备,为其对应的block_device结构体指针*/ 
     struct cdev *i_cdev;   /*若是字符设备,为其对应的cdev结构体指针*/ 
     ... 
 }; 

字符设备驱动

cdev结构体

结构体定义:

复制代码
struct cdev { 
   
    struct kobject kobj; /* 内嵌的kobject对象 */ 
   
    struct module *owner; 	/*所属模块*/  
   
    struct file_operations *ops;  /*文件操作结构体*/ 
   
    struct list_head list; 
  
    dev_t dev;      /*设备号*/ 
   
    unsigned int count; 

}; 

cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位从设备号。

可以使用以下宏获取主从设备号:

复制代码
MAJOR(dev_t dev) 
MINOR(dev_t dev) 

而使用下列宏可以通过主从设备号生成dev_t

复制代码
MKDEV(int major, int minor) 

cdev结构体另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。

file_operations结构体

file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的系统调用时最终被调用。

结构体定义:

复制代码
1struct file_operations { 
2   struct module *owner; 
3      /* 拥有该结构的模块的指针,一般为THIS_MODULES */ 
4    loff_t(*llseek)(struct file *, loff_t, int); 
5      /* 用来修改文件当前的读写位置 */ 
6    ssize_t(*read)(struct file *, char _ _user *, size_t, loff_t*); 
7      /* 从设备中同步读取数据 */ 
8    ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*); 
9      /* 向设备发送数据*/ 
10   ssize_t(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t); 
11     /* 初始化一个异步的读取操作*/ 
12   ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t); 
13     /* 初始化一个异步的写入操作*/ 
14   int(*readdir)(struct file *, void *, filldir_t); 
15     /* 仅用于读取目录,对于设备文件,该字段为 NULL */ 
16   unsigned int(*poll)(struct file *, struct poll_table_struct*); 
17     /* 轮询函数,判断目前是否可以进行非阻塞的读取或写入*/ 
18   int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 
19     /* 执行设备I/O控制命令*/ 
20   long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); 
21     /* 不使用BLK的文件系统,将使用此种函数指针代替ioctl */ 
22   long(*compat_ioctl)(struct file *, unsigned int, unsigned long); 
23     /* 在64位系统上,32位的ioctl调用,将使用此函数指针代替*/ 
24   int(*mmap)(struct file *, struct vm_area_struct*); 
25     /* 用于请求将设备内存映射到进程地址空间*/ 
26   int(*open)(struct inode *, struct file*); 
27     /* 打开 */ 
28   int(*flush)(struct file*); 
29   int(*release)(struct inode *, struct file*); 
30     /* 关闭*/ 
31   int (*fsync) (struct file *, struct dentry *, int datasync); 
32     /* 刷新待处理的数据*/ 
33   int(*aio_fsync)(struct kiocb *, int datasync); 
34     /* 异步fsync */ 
35   int(*fasync)(int, struct file *, int); 
36     /* 通知设备FASYNC标志发生变化*/ 
37   int(*lock)(struct file *, int, struct file_lock*); 
38   ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); 
39     /* 通常为NULL */ 
40   unsigned long(*get_unmapped_area)(struct file *,unsigned long, unsigned long, 
41     unsigned long, unsigned long); 
42     /* 在当前进程地址空间找到一个未映射的内存段 */ 
43   int(*check_flags)(int); 
44     /* 允许模块检查传递给fcntl(F_SETEL...)调用的标志 */ 
45   int(*dir_notify)(struct file *filp, unsigned long arg); 
46     /* 对文件系统有效,驱动程序不必实现*/ 
47   int(*flock)(struct file *, int, struct file_lock*);  
48   ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, 
49    unsigned int); /* 由VFS调用,将管道数据粘接到文件 */ 
50   ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,  
51   unsigned int); /* 由VFS调用,将文件数据粘接到管道 */ 
52   int (*setlease)(struct file *, long, struct file_lock **); 
53 }; 

cdev和inode的关系

cdev结构体的list成员连接到了inode结构体i_devices成员。其中,i_devices也是一个list_head结构。使cdev结构与inode节点组成一个双向链表。inode结构体表示/dev目录下的设备文件。

每个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode节点。这样可以通过inode结点的i_cdev字段找到cdev字符结构体。通过cdev的ops指针,就能找到设备A的操作函数。

上述通过inode节点的某个字段找到结构体的过程使用了container_of()函数

container_of()函数

container_of()的作用是通过结构体成员的指针找到对应结构体的 指针,这个技巧在Linux内核编程中十分常用。在container_of(inode->i_cdev,struct globalmem_dev, cdev)语句中,传给container_of()的第1个参数是结构体成员的指针,第2个参数为整个结构体的 类型,第3个参数为传入的第1个参数即结构体成员的类型,container_of()返回值为整个结构体 的指针。

顶半部和底半部的理解

设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。

下图描述了Linux内核的中断处理机制。为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部和底半部。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行"登记中断"的工作。"登记中断"意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。

现在,中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。

尽管顶半部、底半部的结合能够善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成

自旋锁

自旋锁其实就是信号量的特殊情况,即信号量计数器最大值为1的情形。

那么单CPU系统还有必要使用自旋锁操作吗?

其实是有必要的,自旋锁主要针对SMP(多处理器)或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系 统,自旋锁退化为空操作。在单CPU和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。由于内核可抢占的单CPU系统的行为实际很类似于SMP系统,因此,在这样的单CPU系统 中使用自旋锁仍十分必要。

那自旋锁就那么强大吗?

其实也不是,使用时要特别注意以下问题:

  1. 自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行"测试并设置"该锁直到可 用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。
  2. 自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果 一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。
  3. 自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如 调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核的崩溃。

自旋锁不关心锁定的临界区究竟进行怎样的操作,不管是读还是写,它都一视同仁。即便多 个执行单元同时读取临界资源也会被锁住。实际上,对共享资源并发访问时,多个执行单元同时 读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发。读写自旋锁是 一种比自旋锁粒度更小的锁机制,它保留了"自旋"的概念,但是在写操作方面,只能最多有 1 个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。

platform设备驱动

在Linux设备驱动模型中,我们通常需要关心总线、设备和驱动三个实体,总线将设备和驱动绑定。

系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,都会寻找与之匹配的设备,而匹配由总线完成。

设备和驱动都挂载在一种总线上,这对于本身就依附与PCI、USB、I2C、SPI等设备而言,这自然不是问题;但在嵌入式系统中,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设的确不依附于此类总线。基于此背景,Linux发明了一种虚拟的总线。也就是platform总线,相应的设备称为platform_device,而驱动称为platform_driver。

platform_device结构体原型:

复制代码
 struct platform_device {
    const char * name;    / * 设备名 */  
    u32  id; 
    struct device dev;  
    u32  num_resources;    / * 设备所使用各类资源数量 */ 
    struct resource * resource;  / * 资源 */ 
}; 

驱动的模板总结:

都有probe()、remove()、 suspend()、resume()这样的接口。

spi驱动

在Linux中,常使用spi_master结构体来描述一个SPI主机控制器驱动,其主要成员是主机控制器的序号(系统中可能存在多个SPI主机控制器)、片选数量、SPI模式和时钟设置用到的函数、数据传输用到的函数等。

复制代码
struct spi_master { 
     
    struct device   dev;       
    s16		bus_num; 
    u16		num_chipselect; 
          /* 设置模式和时钟 */ 
    int		(*setup)(struct spi_device *spi);    
      /* 双向数据传输 */ 
    int		(*transfer)(struct spi_device *spi,                                    
                 struct spi_message *mesg); 
  
    void	(*cleanup)(struct spi_device *spi); 
 }; 

分配、注册、和注销SPI主机的API由SPI核心提供:

复制代码
struct spi_master * spi_alloc_master(struct device *host, unsigned size);                                       
int spi_register_master(struct spi_master *master);    
void spi_unregister_master(struct spi_master *master); 

在Linux中,用spi_driver结构体来描述一个SPI外设驱动,可以认为是spi_master的client驱动。

复制代码
struct spi_driver { 
    int                     (*probe)(struct spi_device *spi); 
    int                     (*remove)(struct spi_device *spi); 
    void                    (*shutdown)(struct spi_device *spi); 
    int                     (*suspend)(struct spi_device *spi, pm_message_t mesg); 
    int                     (*resume)(struct spi_device *spi); 
    struct device_driver    driver; 
};

可以看出,spi_driver结构体和platform_driver结构体有极大的相似性,都有probe()、remove()、 suspend()、resume()这样的接口。是的,这几乎是一切client驱动的习惯模板。

SPI传输实例

复制代码
static inline int 
  spi_write(struct spi_device *spi, const u8 *buf, size_t len) 
  { 
          struct spi_transfer t = { 
              .tx_buf         = buf, 
              .len            = len, 
            }; 
          struct spi_message m; 
   
         spi_message_init(&m); 
         spi_message_add_tail(&t, &m); 
         return spi_sync(spi, &m); 
 } 
 
 static inline int 
 spi_read(struct spi_device *spi, u8 *buf, size_t len) 
 { 
         struct spi_transfer t = { 
             .rx_buf         = buf, 
             .len            = len, 
            }; 
         struct spi_message m; 
         spi_message_init(&m); 
         spi_message_add_tail(&t, &m); 
         return spi_sync(spi, &m); 
 } 

SPI只是一种总线,spi_driver 的作用只是将SPI外设挂接在该总线上,因此在spi_driver的probe()成员函数中,将注册SPI外设本身所属设备驱动的类型。

misc设备驱动

Linux包含了许多的设备驱动类型,但不管分的多细,总会由漏网之鱼。所以在Linux里面。把无法归类的五花八门的设备定义为混杂设备(用miscdevice结构体描述)。Linux内核所提供的miscdevice有很强的包容性。

miscdevice共享一个主设备号MISC_MAJOR(即10),但次设备号不同。所有的miscdevice设备形成一个链表,对设备访问时内核会根据次设备号查找对应的miscdevice设备,然后再调用其file_operations结构体中注册的文件操作接口进行操作。

基于sysfs的设备驱动

一些设备驱动以sysfs结点的形式存在,其本身没有对应的/dev结点;一些设备驱动虽然有对应的/dev结点,也依赖于sysfs结点进行一些操作。

它以sysdev_drvier进行描述,结构体定义如下:

复制代码
 struct sysdev_driver { 
    struct list_head        entry; 
    int     (*add)(struct sys_device *); 
    int     (*remove)(struct sys_device *); 
    int     (*shutdown)(struct sys_device *); 
    int     (*suspend)(struct sys_device *, pm_message_t state); 
    int     (*resume)(struct sys_device *); 
};

用户可以手动cat、echo来操作这些结点或者使用cpufrequtils工具访问这些结点以与内核通信。

块设备驱动

块设备与字符设备的不同:

  1. 块设备只能以块为单位接受输入和输出,而字符设备则以字节为单位。大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。
  2. 块设备对于IO请求有缓冲区,因此它们可以选择以什么顺序进行响应,因为在读写连续的扇区比分离的扇区更快
  3. 字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可以随机访问,但顺序地组织块设备的访问可以提高性能。

在块设备驱动中,有一个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合。

block_device_operations结构体

复制代码
struct block_device_operations {      
    int (*open) (struct block_device *, fmode_t);          
    int (*release) (struct gendisk *, fmode_t);       
    int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); 
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);        
    int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);          
    int (*direct_access) (struct block_device *, sector_t,                                                  
    void **, unsigned long *);       
    int (*media_changed) (struct gendisk *);       
    int (*revalidate_disk) (struct gendisk *);       
    int (*getgeo)(struct block_device *, struct hd_geometry *);       
    struct module *owner;  
}; 

在Linux内核中,使用gendisk结构体来表示一个独立的磁盘设备

gendisk(通用磁盘)结构体

复制代码
struct gendisk {           
    int major;                       /* 主设备号 */ 
             
    int first_minor;        
    int minors;                     /* 最大的次设备数,如果不能分区,则为1*/  
    char disk_name[DISK_NAME_LEN];   /* 设备名称 */ 

      /* 由partno索引的分区指针的数组            */ 
    struct disk_part_tbl *part_tbl; 
    struct hd_struct part0; 

    struct block_device_operations *fops; /* 块设备操作集合 */ 
    struct request_queue *queue; 
    void *private_data; 

    int flags; 
    struct device *driverfs_dev;  // FIXME: remove 
    struct kobject *slave_dir; 

    struct timer_rand_state *random; 

    atomic_t sync_io;               /* RAID */ 
    struct work_struct async_notify; 
#ifdef  CONFIG_BLK_DEV_INTEGRITY 
    struct blk_integrity *integrity; 
#endif 
    int node_id; 
}; 

major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一 个主设备号,而次设备号则不同。

fops为block_device_operations,即上节描述的块设备操作集合。

queue是内核用来管理这个设备的I/O请求队列的指针。

private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。hd_struct成员表示一个分区,而 disk_part_tbl成员用于容纳分区表,part0和part_tbl二者的关系在于

复制代码
disk->part_tbl->part[0] = &disk->part0;
  1. 分配gendisk

    struct gendisk *alloc_disk(int minors);

  2. 增加gendisk

    void add_disk(struct gendisk *disk);

  3. 释放gendisk

    void del_gendisk(struct gendisk *gp);

  4. gendisk引用计数

    struct kobject *get_disk(struct gendisk *disk);
    void put_disk(struct gendisk *disk);

学习参考链接

https://www.nowcoder.com/issue/tutorial?zhuanlanId=jLN8b0&uuid=327d2cd1e9f4495190f9361484ce623a

相关推荐
行思理2 小时前
Linux多PHP如何切换系统默认PHP版本
linux·运维·php
AI视觉网奇2 小时前
图生3d 人脸 算法笔记 2025
笔记·3d
charlie1145141912 小时前
现代C++工程实践:简单的IniParser4——实现ini_parser
开发语言·c++·笔记·学习·工程
jimy12 小时前
ps aux|grep pid 和 ps -p pid 的区别
java·linux·开发语言
weixin_437546332 小时前
注释文件夹下脚本的Debug
java·linux·算法
zfj3213 小时前
容器 的 cpu request limit 与 linux cgroups 的关系
linux·运维·服务器·kubernetes·cgroup
长桥夜波3 小时前
【第二十三周】统计学习复习笔记
笔记·学习
AA陈超3 小时前
LyraStarterGame 5.6 项目学习路径
c++·笔记·学习·lyra
Lueeee.3 小时前
Linux内核镜像分析
linux·服务器