Linux设备驱动开发详解2

简介: Linux设备驱动开发详解

Linux设备驱动开发详解1:https://developer.aliyun.com/article/1597414


四、Linux内核模块

1、Linux内核模块程序结构

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

  1. 模块加载函数
    当通过 insmodmodprobe 命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
  2. 模块卸载函数

当通过 rmmod 命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。

  1. 模块许可证声明
    许可证(LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。
  2. 模块参数(可选)
    模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
  3. 模块导出符号(可选)

内核模块可以导出的符号(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

image.png

目录
相关文章
|
12天前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
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 设备驱动程序(二)
15 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1
|
11天前
|
Linux API
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】