Linux设备驱动开发详解

在这里插入图片描述

Linux 内核系列文章


Linux 内核设计与实现
深入理解 Linux 内核
Linux 设备驱动程序
Linux设备驱动开发详解
深入理解Linux虚拟内存管理



前言

  本文主要用来摘录《Linux设备驱动开发详解第四版》一书中学习知识点,本书基于 Linux 4.0 版本,源代码摘录基于 Linux 4.15.18 ,两者之间可能有些出入。

资源链接: 宋宝华《Linux设备驱动开发详解》


一、Linux设备驱动概述及开发环境构建

  • 设备驱动的作用
  • 无操作系统时的设备驱动
  • 有操作系统时的设备驱动
  • Linux设备驱动
  • Linux设备驱动的开发环境构建
  • 设备驱动Hello World:LED驱动

二、驱动设计的硬件基础

  • 处理器
  • 存储器
  • 接口与总线
  • CPLD和FPGA
  • 原理图分析
  • 硬件时序分析
  • 芯片数据手册阅读方法
  • 仪器仪表使用

三、 Linux内核及内核编程

1、Linux内核的发展与演变

    Linux 操作系统是 UNIX 操作系统的一种克隆系统,是一种类 UNIX 操作系统,诞生于1991年10月5日(第一次正式向外公布的时间),起初的作者是 Linus Torvalds。Linux 操作系统的诞生、发展和成长过程依赖着 5 个重要支柱:UNIX 操作系统、Minix 操作系统、GNU 计划、POSIX 标准和 Internet

2、Linux 2.6后的内核特点

  1. 新的调度器
  2. 内核抢占
  3. 改进的线程模型
  4. 虚拟内存的变化
  5. 文件系统
  6. 音频
  7. 总线、设备和驱动模型
  8. 电源管理
  9. 联网和IPSec
  10. 用户界面层
  11. Linux 3.0后ARM架构的变更

3、Linux内核的组成

(1)Linux内核源代码的目录结构

  • arch:包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如 i386、arm、arm64、
    powerpc、mips 等。Linux 内核目前已经支持 30 种左右的体系结构。在 arch 目录下,存放的是各个平台以及各个平台的芯片对 Linux 内核进程调度、内存管理、中断等的支持,以及每个具体的 SoC 和电路板的板级支持代码。
  • block:块设备驱动程序 I/O 调度。
  • crypto:常用加密和散列算法(如 AES、SHA 等),还有一些压缩和 CRC 校验算法。
  • documentation:内核各部分的通用解释和注释。
  • drivers:设备驱动程序,每个不同的驱动占用一个子目录,如 char、block、net、mtd、i2c 等。
  • fs:所支持的各种文件系统,如 EXT、FAT、NTFS、JFFS2 等。
  • include:头文件,与系统相关的头文件放置在 include/linux 子目录下。
  • init:内核初始化代码。著名的 start_kernel() 就位于 init/main.c 文件中。
  • ipc:进程间通信的代码。
  • kernel:内核最核心的部分,包括进程调度、定时器等,而和平台相关的一部分代码放 arch/*/kernel 目录下。
  • lib:库文件代码。
  • mm:内存管理代码,和平台相关的一部分代码放在 arch/*/mm 目录下。
  • net:网络相关代码,实现各种常见的网络协议。
  • scripts:用于配置内核的脚本文件。
  • security:主要是一个 SELinux 的模块。
  • sound:ALSA、OSS 音频设备的驱动核心代码和常用设备驱动。
  • usr:实现用于打包和压缩的 cpio 等。
  • include:内核API级别头文件。

    内核一般要做到 driversarch 的软件架构分离,驱动中不包含板级信息,让驱动跨平台。同时内核的通用部分(如 kernel、fs、ipc、net 等)则与具体的硬件( arch 和 drivers )剥离。

(2)Linux内核的组成部分

    Linux 内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、
网络接口(NET)和进程间通信(IPC)5 个子系统组成。

① 进程调度

在这里插入图片描述

② 内存管理

在这里插入图片描述

【操作系统】进程空间管理

在这里插入图片描述
在这里插入图片描述

③ 虚拟文件系统

    Linux 虚拟文件系统隐藏了各种硬件的具体细节,为所有设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象。它为上层的应用程序提供了统一的 vfs_read()vfs_write() 等接口,并调用具体底层文件系统或者设备驱动中实现的 file_operations 结构体的成员函数。

在这里插入图片描述

④ 网络接口

    网络接口提供了对各种网络标准的存取和各种网络硬件的支持。

在这里插入图片描述

⑤ 进程间通信

    Linux 支持进程间的多种通信机制,包含信号量、共享内存、消息队列、管道、UNIX 域套接字等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。在实际的 Linux 应用中,人们更多地趋向于使用 UNIX 域套接字,而不是 System V IPC 中的消息队列等机制。Android 内核则新增了 Binder 进程间通信方式。

    Linux 内核 5 个组成部分之间的依赖关系如下。

  • 进程调度与内存管理之间的关系:这两个子系统互相依赖。在多程序环境下,程序要运行,则必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。
  • 进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间之外,还可以存取共同的内存区域。
  • 虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持 RAMDISK 设备。
  • 内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程定期由调度程序调度,这也是内存管理依赖于进程调度的原因。当一个进程存取的内存映射被换出时,内存管理向虚拟文件系统发出请求,同时,挂起当前正在运行的进程。

    除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的 API ,如分配和释放内存空间的函数、输出警告或错误消息的函数及系统提供的调试接口等。

(3)Linux内核空间与用户空间

    ARM Linux 的系统调用实现原理是采用 swi 软中断从用户(usr)模式陷入管理模式(svc)。

4、Linux内核的编译及加载

(1)Linux内核的编译

make config 		#(基于文本的最为传统的配置界面,不推荐使用)
make menuconfig		#(基于文本菜单的配置界面)
make xconfig		#(要求QT被安装)
make gconfig		#(要求GTK+被安装)

    内核配置包含的条目相当多,arch/arm/configs/xxx_defconfig 文件包含了许多电路板的默认配置。只需要运行 make ARCH=arm xxx_defconfig 就可以为 xxx 开发板配置内核。

编译内核和模块的方法是:

make ARCH=arm xxx_defconfig		# 使用默认配置
make ARCH=arm zImage
make ARCH=arm modules

    上述命令中,如果 ARCH=arm 已经作为环境变量导出,则不再需要在 make 命令后书写该选项。执行完上述命令后,在源代码的根目录下会得到未压缩的内核映像 vmlinux 和内核符号表文件 System.map ,在 arch/arm/boot/ 目录下会得到压缩的内核映像 zImage,在内核各对应目录内得到选中的内核模块。

(2)Kconfig 和 Makefile

    在 Linux 内核中增加程序需要完成以下 3 项工作。

  • 将编写的源代码复制到 Linux 内核源代码的相应目录中。
  • 在目录的 Kconfig 文件中增加关于新源代码对应项目的编译配置选项。
  • 在目录的 Makefile 文件中增加对新源代码的编译条目。

(3)Linux内核的引导

在这里插入图片描述

四、Linux内核模块

1、Linux内核模块程序结构

    一个 Linux 内核模块主要由如下几个部分组成。

  1. 模块加载函数
    当通过 insmodmodprobe 命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
  2. 模块卸载函数
    当通过 rmmod 命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
  3. 模块许可证声明
    许可证(LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。
  4. 模块参数(可选)
    模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
  5. 模块导出符号(可选)
    内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的变量或函数。
  6. 模块作者等信息声明(可选)

五、Linux文件系统与设备文件

1、Linux文件系统

在这里插入图片描述
在这里插入图片描述

2、udev 用户空间设备管理

udev 规则文件介绍

    udev 完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件(Hotplug Event)来工作。在热插拔时,设备的详细信息会由内核通过 netlink 套接字发送出来,发出的事情叫 ueventudev 的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用从内核收到的信息来进行创建设备文件节点等工作。

netlink 的使用范例

#include <linux/netlink.h>

static void die(char *s) {
  write(2, s, strlen(s));
  exit(1);
}

int main(int argc, char *argv[]) {
  struct sockaddr_nl nls;
  struct pollfd pfd;
  char buf[512];

  // Open hotplug event netlink socket

  memset(&nls, 0, sizeof(struct sockaddr_nl));
  nls.nl_family = AF_NETLINK;
  nls.nl_pid = getpid();
  nls.nl_groups = -1;

  pfd.events = POLLIN;
  pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
  if (pfd.fd == -1)
    die("Not root\n");

  // Listen to netlink socket
  if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
    die("Bind failed\n");
  while (-1 != poll(&pfd, 1, -1)) {
    int i, len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
    if (len == -1)
      die("recv\n");

    // Print the data to stdout.
    i = 0;
    while (i < len) {
      printf("%s\n", buf + i);
      i += strlen(buf + i) + 1;
    }
  }
  die("poll\n");

  // Dear gcc: shut up.
  return 0;
}

查找规则文件能利用的内核信息和sysfs属性信息

udevadm info -a -p /sys/devices/platform/serial8250/tty/ttyS0

如果/dev/下面的节点已经被创建,但是不知道它对应的/sys具体节点路径,可以采用命令反向分析

udevadm info -a -p $(udevadm info -q path -n /dev/<节点名>)

    在嵌入式系统中,也可以用 udev 的轻量级版本 mdev ,mdev 集成于 busybox 中。在编译 busybox 的时候,选中 mdev 相关项目即可。

    Android 也没有采用 udev,它采用的是 voldvold 的机制和 udev 是一样的,理解了 udev ,也就理解了 voldAndroid 的源代码 NetlinkManager.cpp 同样是监听基于 netlink 的套接字,并解析收到的消息。

六、字符设备驱动

1、 Linux字符设备驱动结构

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
} __randomize_layout;

void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
						const char *name);
void unregister_chrdev_region(dev_t from, unsigned count);

file_operations 结构说明:

  • unlocked_ioctl() 提供设备相关控制命令的实现(既不是读操作,也不是写操作),当调用成功时,返回给调用程序一个非负值。它与用户空间应用程序调用的 int fcntl(int fd, int cmd, …/arg/)int ioctl(int d, int request, …) 对应。

  • mmap() 函数将设备内存映射到进程的虚拟地址空间中,如果设备驱动未实现此函数,用户进行 mmap() 系统调用时将获得 -ENODEV 返回值。这个函数对于帧缓冲等设备特别有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问它而无须在内核和应用间进行内存复制。它与用户空间应用程序中的 void * mmap(void*addr, size_t length, int prot, int flags, int fd, off_t offset) 函数对应。

  • poll() 函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行 select()poll() 系统调用将引起进程的阻塞。

  • aio_read()aio_write() 函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符执行 SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy 等系统调用进行读写。

七、Linux设备驱动中的并发控制

  • 原子操作
  • 自旋锁
  • 信号量
  • 互斥体
  • 完成量

八、Linux设备驱动中的阻塞与非阻塞I/O

1、阻塞与非阻塞I/O

// include/linux/wait.h

// 定义“等待队列头部”
wait_queue_head_t my_queue;

// 初始化“等待队列头部”
init_waitqueue_head(&my_queue);

// 定义等待队列元素
DECLARE_WAITQUEUE(name, tsk)

// 添加/移除等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

// 等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

// 唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

    在设备驱动中阻塞 I/O 一般基于等待队列或者基于等待队列的其他 Linux 内核 API 来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞 I/O 的应用程序也可借助轮询函数来查询设备是否能立即被访问,用户空间调用 select()poll() 或者 epoll() 接口,设备驱动提供 poll() 函数。设备驱动的 poll() 本身不会阻塞,但是与 poll() 、select() 和 epoll() 相关的系统调用则会阻塞地等待至少一个文件描述符集合可访问或超时。

九、Linux设备驱动中的异步通知与异步I/O

1、Linux信号

在这里插入图片描述

在这里插入图片描述

2、安装信号

void (*signal(int signum, void (*handler))(int)))(int);

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

3、Linux 异步 I/O

    glibcAIO 主要包括如下函数:

#include <aio.h>
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
int aio_error( struct aiocb *aiocbp );
ssize_t aio_return( struct aiocb *aiocbp );
int aio_suspend( const struct aiocb *const cblist[],
				int n, const struct timespec *timeout );
int aio_cancel(int fd, struct aiocb *aiocbp);
int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );
#include <linux/aio_abi.h>
int io_setup(int maxevents, io_context_t *ctxp);
int io_destroy(io_context_t ctx);
int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);
int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt);
int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events,
struct timespec *timeout);
void io_set_callback(struct iocb *iocb, io_callback_t cb);
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,
long long offset);
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,
long long offset);

4、AIO与设备驱动

ssize_t (*aio_read) (struct kiocb *iocb, const struct iovec *iov, unsigned long
					nr_segs, loff_t pos);
ssize_t (*aio_write) (struct kiocb *iocb, const struct iovec *iov, unsigned
					long nr_segs, loff_t pos);
int (*aio_fsync) (struct kiocb *iocb, int datasync);

十、中断与时钟

在这里插入图片描述

1、Linux中断编程

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
				const char *name, void *dev);
  • irq 是要申请的硬件中断号。
  • handler 是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev 参数将被传递给它。
  • irqflags 是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是 IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW 等。在处理方式方面,若设置了 IRQF_SHARED,则表示多个设备共享中断,dev 是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL。

    request_irq() 返回 0 表示成功,返回 -EINVAL 表示中断号无效或处理函数指针为 NULL,返回 -EBUSY 表示中断已经被占用且不能共享。

int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
					unsigned long irqflags, const char *devname, void *dev_id);

    此函数与 request_irq() 的区别是 devm_ 开头的 API 申请的是内核 “managed” 的资源,一般不需要在出错处理和 remove() 接口里再显式的释放。有点类似 Java 的垃圾回收机制。

void free_irq(unsigned int irq,void *dev_id);

(1)使能和屏蔽中断

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

2、底半部机制

    Linux 实现底半部的机制主要有 tasklet、工作队列、软中断和线程化 irq

    软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet 是基于软中断实现的,因此也运行于软中断上下文。

    在 Linux 内核中,用 softirq_action 结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用 open_softirq() 函数可以注册软中断对应的处理函数,而 raise_softirq() 函数可以触发一个软中断。

    软中断和 tasklet 运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和 tasklet 处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。

    local_bh_disable()local_bh_enable() 是内核中用于禁止和使能软中断及 tasklet 底半部机制的函数。

    内核中采用 softirq 的地方包括 HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ 等,一般来说,驱动的编写者不会也不宜直接使用 softirq 。

    需要特别说明的是,软中断以及基于软中断的 tasklet 如果在某段时间内大量出现的话,内核会把后续软中断放入 ksoftirqd 内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
						irq_handler_t thread_fn,
						unsigned long flags, const char *name, void *dev);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
						irq_handler_t handler, irq_handler_t thread_fn,
						unsigned long irqflags, const char *devname,
						void *dev_id);

    由此可见,它们比 request_irq() 、devm_request_irq() 多了一个参数 thread_fn。用这两个 API 申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过 request_threaded_irq() 申请,自然会得到新的内核线程。

3、内核延时

void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);

    上述函数将使得调用它的进程睡眠参数指定的时间为 millisecs,msleep() 、ssleep() 不能被打断,而 msleep_interruptible() 则可以被打断。

#define time_after(a,b) \
		(typecheck(unsigned long, a) && \
		typecheck(unsigned long, b) && \
		((long)(b) - (long)(a) < 0))
		
#define time_before(a,b) time_after(b,a)

    睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU 资源被其他进程使用。schedule_timeout() 可以使当前任务休眠至指定的 jiffies 之后再重新被调度执行,msleep() 和 msleep_interruptible() 在本质上都是依靠包含了 schedule_timeout() 的 schedule_timeout_uninterruptible() 和 schedule_timeout_interruptible() 来实现的。

    实际上,schedule_timeout() 的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。

    下面两个函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒(后者可以在超时前被打断):

sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);

十一、内存与I/O访问

符号

   
⇐ ⇒ ⇔ ⇆ ⇒ ⟺
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑿⒀⒁⒂⒃⒄⒅⒆⒇
➊➋➌➍➎➏➐➑➒➓⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵
ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ
🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩

123

y = x 2 + z 3 y = x^2 + z_3 y=x2+z3

y = x 2 + z 3 + a b + b a y = x^2 + z_3 + \frac {a}{b} + \sqrt[a]{b} y=x2+z3+ba+ab

y = x 2 + z 3 (1) y = x^2 + z^3 \tag{1} y=x2+z3(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值