• 【Linux 驱动基础】驱动程序基石


    # 休眠与唤醒

    休眠唤醒原理

            所谓休眠就是把自己的状态改为非 RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒 APP1。所以唤醒就是把程序的状态改为 RUNNING,这样内核的调度器有合适的时间就会让它运行。

    休眠唤醒流程

    1. APP调用read等函数;
    2. APP进入内核态read函数,等待按键事件发生,开始休眠;
    3. 当按键事件发生时,驱动程序的中断服务程序被调用,它会读取按键值并唤醒APP;
    4. 驱动中read被唤醒,将按键值返回给应用层;
    5. 应用层read函数返回

    休眠唤醒框架

    如上图所示是休眠唤醒的驱动框架,根据该框架,我们需要做这几件事情:

    • 初始化wq队列,使用DECLARE_WAIT_QUEUE_HEAD()宏函数实现。
    • 在驱动层的drv_read中,调用wait_event_interruptible()函数:
    • 它本身会判断event参数是否为FALSE,如果为FASLE表示无数据,会将自己放入wq等待队列并休眠。
    • 当从wait_event_interruptible()函数返回后,会继续把数据复制回用户空间。
    • 在中断服务程序中,设置event为TRUE,并且调用wake_up_interruptible唤醒正在队列中休眠的线程/进程

    驱动编程

            参考内核源码: include\linux\wait.h

    休眠函数:

    • wq:waitqueue,等待队列。
      • 休眠时除了把程序状态改为非RUNNING之外,还要把进程/进程放入wq中,以后中断服务程序要从wq中把它取出来唤醒。
    • condition:可以是变量,也可以是任何表达式,表示一直等待,直到condition为真

    唤醒函数: 

    # POLL 机制

    poll流程

    APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间。

    • APP进入内核态,调用到驱动程序的drv_poll函数,如果有数据的话立刻返回。
    • 如果发现没有数据时就休眠一段时间。
      • 当按下按键时,驱动程序的中断服务程序被调用,它会记录数据并且唤醒APP。
      • 当超时时间到了之后,内核也会唤醒APP。
    • APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用read得到数据。

    如上图所示poll机制执行的流程,分为9步:

            1. APP使用open系统调用打开按键设备。

            2. 通过内核文件系统中sys_open函数调用file_operation结构体中的drv_open驱动函数来打开设备。

    每一个设备,内核都会将其看成是一个文件,都会在内核中创建一个struct file结构体来描述这个设备,该结构体就位于内核的文件系统中。

            3. APP调用poll系统调用后进入内核态。

            4. 内核文件系统中的sys_poll,会在死循环for中,先调用驱动程序的drv_poll来获取状态event。

                    在驱动程序drv_poll中,要把当前线程挂入到等待队列wq中,否则在唤醒的时候就找不到该线程了。
                    但是驱动程序drv_poll并不会让当前线程休眠。
            5. 返回的状态表示当前没有数据,那么内核文件系统就让该线程休眠一会儿。

            6. 线程休眠过程中,按下了按键,产生了按键中断,在中断服务函数中记录按键值,并且从wq等待队列中将线程唤醒。

            7. 线程被唤醒后处于内核文件系统中的for死循环中,所以还要再执行一次drv_poll驱动程序,获取按键数据的状态。

            8. 此时获取到的数据状态表示有按键数据,就会从返回到用户态,APP可以继续执行不再阻塞。

            9. APP根据poll的返回值发现有按键数据,则调用read函数读取按键数据。

    如果一直没有按键数据,也就是线程在休眠后一直没有被唤醒,此时的流程也是类似的,从第三步开始看:

            3. APP调用poll系统调用后进入内核态。
            4. 导致驱动程序的drv_poll被调用。
            5. 假设当前没有数据,则休眠一会。
            6. 在休眠过程中,一直没有按下了按键,超时时间到了,内核把这个线程唤醒。
            7. 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll驱动程序获取数据状态。
            8. 此时获取到的数据状态仍然表示没有数据,但是超时时间已经到了,也只能从内核态返回用户态了。
            9. APP根据poll的返回值发现没有按键数据,则不能调用read函数读取按键数据。

    这个过程中有几点需要注意:

    • drv_poll要把线程挂入队列wq,但是并不是在drv_poll中进入休眠,而 是在调用drv_poll之后休眠。

    drv_poll驱动程序只做两件事情:

    • 把线程放入到等待队列wq中,但是不休眠。
    • 返回event 事件状态,而不是返回事件值。

    APP调用一次poll,有可能会导致drv_poll被调用两次:

    • 进入内核文件系统的for循环中先调用一次获取状态。
    • 被唤醒后(中断或者超时)执行for循环再调用一次,判断是数据到来还是超时,然后返回用户态。

    APP要判断poll返回的原因:

    • 有数据到来,还是超时。有数据到来才能调用read函数读取按键值

    驱动编程

    使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。在drv_poll 函数中要做 2 件事:

            1. 把当前线程挂入队列 wq: poll_wait

                    a) APP 调用一次 poll,可能导致 drv_poll 被调用 2 次,但是我们并不需要把当前线程挂入队列 2 次。

                    b) 可以使用内核的函数 poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。

            2. 返回设备状态:

                    APP 调用 poll 函数时,有可能是查询“有没有数据可以读”: POLLIN, 也有可能是查询“你有没有空间给我写数据”: POLLOUT。所以 drv_poll 要返回自己的当前状态: (POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。

                    a) POLLRDNORM 等同于 POLLIN,为了兼容某些 APP 把它们一起返回。

                    b) POLLWRNORM 等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。

    1. static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    2. {
    3. if (p && p->_qproc && wait_address)
    4. p->_qproc(filp, wait_address, p);
    5. }

    # 异步通知

      异步通知流程

          

    1. 使用open系统调用打开驱动,得到驱动的文件描述符fd。
    2. 使用signal系统调用为SIGIO信号注册信号处理函数func。
      • 按键驱动程序发出的信号是SIGIO信号,表示有数据输入。
      • APP收到SIGIO信号后,处理函数func就会被自动调用。
    3. 使用fcntl将当前进程的PID设置到内核文件系统中的struct file结构体中,方便后面驱动程序找到进程。
    4. 读取驱动程序文件的Flag。
    5. 设置Flag里面的FASYNC位为 1:
      • 该Flag也是记录到内核文件系统的struct file结构体中,驱动程序通过struct file* filp指针可以获取该标志。
      • 当FASYNC位发生变化时,内核文件系统就会调用驱动程序的drv_fasync函数。
    6. drv_fasync是否调用是由FASYNC标志位决定的,应用层并没有相应的fasync函数。
    7. 在drv_fasync函数中,调用内核提供的faync_helper函数,它会根据FAYSNC的值决定是否设置button_async->fa_file=驱动文件filp:
      • 驱动文件filp结构体里面含有之前设置的PID。
      • on就代表是着FAYSNC位,它为1则设置button_async,它为0则不设置。
    8. APP可以做其他事情。
    9. 按键按下后,产生按键中断,调用中断服务函数。
    10. 在中断服务函数中调用内核提供的kill_fasync函数,向APP发送信号:
      • 如果button_async->fa_file非空,则从它指向的filp的结构体中取出进程的PID,向该线程发送SIGIO信号。
      • 如果button_async->fa_file为空,则该函数什么都不做,不会发送任何信号。
    11. 在按键中断服务程序中发送SIGIO信号后,信号处理函数func被调用。
    12. 在func中使用read系统调用读取按键数据。
    13. 最终会调用驱动层中的drv_read读取按键数据,此时一定是有数据的,并不会休眠。

    在整个流程中可以看到,FASYNC的变化是为了启动drv_fasync,而真正做事情的是fasync_helper函数。

    驱动编程 

    使用异步通知时,驱动程序的核心有 2:

            ① 提供对应的 drv_fasync 函数;

            ② 并在合适的时机发信号

    1. /*
    2. * fasync_helper() is used by almost all character device drivers
    3. * to set up the fasync queue, and for regular files by the file
    4. * lease code. It returns negative on error, 0 if it did no changes
    5. * and positive if it added/deleted the entry.
    6. */
    7. int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
    1. /*
    2. #define SIGHUP 1
    3. #define SIGINT 2
    4. #define SIGQUIT 3
    5. #define SIGILL 4
    6. #define SIGTRAP 5
    7. #define SIGABRT 6
    8. #define SIGIOT 6
    9. #define SIGBUS 7
    10. #define SIGFPE 8
    11. #define SIGKILL 9
    12. #define SIGUSR1 10
    13. #define SIGSEGV 11
    14. #define SIGUSR2 12
    15. #define SIGPIPE 13
    16. #define SIGALRM 14
    17. #define SIGTERM 15
    18. #define SIGSTKFLT 16
    19. #define SIGCHLD 17
    20. #define SIGCONT 18
    21. #define SIGSTOP 19
    22. #define SIGTSTP 20
    23. #define SIGTTIN 21
    24. #define SIGTTOU 22
    25. #define SIGURG 23
    26. #define SIGXCPU 24
    27. #define SIGXFSZ 25
    28. #define SIGVTALRM 26
    29. #define SIGPROF 27
    30. #define SIGWINCH 28
    31. #define SIGIO 29
    32. #define SIGPOLL SIGIO
    33. */
    34. void kill_fasync(struct fasync_struct **fp, int sig, int band)

    # 定时器

            所谓定时器,就是闹钟,时间到后你就要做某些事。有 2 个要素:时间、做事,换成程序员的话就是:超时时间、函数
     

    函数说明
    setup_timer(timer, fn, data)设置定时器,主要是初始化 timer_list 结构体,设置其中的函数、超时事件等参数
    void add_timer(struct timer_list *timer)向内核添加定时器

    int mod_timer(struct timer_list *timer, unsigned long expires)

    修改定时器的超时时间
    int del_timer(struct timer_list *timer)删除定时器
    1. struct timer_list {
    2. /*
    3. * All fields that change during normal runtime grouped to the
    4. * same cacheline
    5. */
    6. struct list_head entry;
    7. unsigned long expires; /* 定时器超时时间 */
    8. struct tvec_base *base;
    9. void (*function)(unsigned long); /* 超时函数 */
    10. unsigned long data; /* 超时函数的参数 */
    11. int slack;
    12. #ifdef CONFIG_TIMER_STATS
    13. int start_pid;
    14. void *start_site;
    15. char start_comm[16];
    16. #endif
    17. #ifdef CONFIG_LOCKDEP
    18. struct lockdep_map lockdep_map;
    19. #endif
    20. };

    定时器时间 

    编译内核时,可以在内核源码根目录下用ls -a看到一个隐藏文件,它就 是内核配置文件。打开后可以看到CONFIG_HZ=100,它表示内核每秒中会发生 100 次系统滴答中断(tick)。

    每发生一次 tick 中断,全局变量jiffies就会累加1,定时器的时间就是基于 jiffies的,我们修改超时时间时,一般使用这2种方法:

    • 在add_timer之前,直接修改:
      • timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
      • timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
    • 在add_timer之后,使用mod_timer修改:
      • mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
      • mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒

    # 中断下半部 

    中断下半部分详解

    tasklet

    tasklet结构体:

    1. struct tasklet_struct
    2. {
    3. struct tasklet_struct *next;
    4. unsigned long state;
    5. atomic_t count;
    6. void (*func)(unsigned long);
    7. unsigned long data;
    8. };

    ⚫ 其中的 state 有 2 位:
        ◼ bit0 表示 TASKLET_STATE_SCHED等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了;tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把tasklet 放入队列。
            ◼ bit1 表示 TASKLET_STATE_RUN等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。
    ⚫ 其中的 count 表示该 tasklet 是否使能:等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的 tasklet,里面的 func 函数不会被执行。

    1. 初始化tasklet结构体

    1. #define DECLARE_TASKLET(name, func, data) \
    2. struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
    3. #define DECLARE_TASKLET_DISABLED(name, func, data) \
    4. struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
    5. void tasklet_init(struct tasklet_struct *t,
    6. void (*func)(unsigned long), unsigned long data);

            ◼ 使用 DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;

            ◼ 使用 DECLARE_TASKLET_DISABLED 定义的 tasklet 结构体,它是禁止的;使用之前要先调用 tasklet_enable 使能它

     2. 使能/禁止 tasklet

    1. static inline void tasklet_enable(struct tasklet_struct *t);
    2. static inline void tasklet_disable(struct tasklet_struct *t);

            ◼ tasklet_enable 把 count 增加 1;

            ◼ tasklet_disable 把 count 减 1。

    3. 调度 tasklet 

    static inline void tasklet_schedule(struct tasklet_struct *t);

            ◼ 把 tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为1 

    4. kill tasklet 

    extern void tasklet_kill(struct tasklet_struct *t);

    ◼ 如 果 一 个 tasklet 未 被 调 度 , tasklet_kill 会 把 它 的TASKLET_STATE_SCHED 状态清 0;

    ◼ 如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再把它的 TASKLET_STATE_SCHED 状态清 0。

     工作队列

    内核线程、工作队列(workqueue)都由内核创建了,我们只是使用。使用的核心是一个 work_struct 结构体,定义如下:

    使用工作队列时,步骤如下:

            第1步 构造一个 work_struct 结构体,里面有函数;

            第2步 把这个 work_struct 结构体放入工作队列,内核线程就会运行 work 中的函数

     1.  定义 work

    1. #define DECLARE_WORK(n, f) \
    2. struct work_struct n = __WORK_INITIALIZER(n, f)
    3. #define DECLARE_DELAYED_WORK(n, f) \
    4. struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
    5. #define INIT_WORK(_work, _func)

    ⚫ 第 1 个宏是用来定义一个 work_struct 结构体,要指定它的函数。

    ⚫ 第 2 个宏用来定义一个 delayed_work 结构体,也要指定它的函数。所以“ delayed”,意思就是说要让它运行时,可以指定:某段时间之后你再执行。

    ⚫如果要在代码中初始化 work_struct 结构体,可以使用 INIT_WORK

    2. 使用 work

    1. /**
    2. * schedule_work - put work task in global workqueue
    3. * @work: job to be done
    4. *
    5. * Returns %false if @work was already on the kernel-global workqueue and
    6. * %true otherwise.
    7. *
    8. * This puts a job in the kernel-global workqueue if it was not already
    9. * queued and leaves it in the same position on the kernel-global
    10. * workqueue otherwise.
    11. */
    12. static inline bool schedule_work(struct work_struct *work)
    13. {
    14. return queue_work(system_wq, work);
    15. }

    3. 其他函数 

    # 中断的线程化处理

    https://www.cnblogs.com/fuzidage/p/17580922.html

  • 相关阅读:
    dotConnect for Oracle .net Crack
    面试时、软件测试技术不是你的拦路虎,HR才是你拦路人
    前端性能优化手段
    ApplicationContextAware、ApplicationContext
    2022/8/11 状压+矩阵快速幂
    预编译为什么能防止SQL注入?一看你就明白了。预编译原理详解
    深度学习入门(五十二)计算机视觉——风格迁移
    Photographic Tone Reproduction for Digital Images
    【python】一篇玩转正则表达式
    Node.js学习笔记_No.03
  • 原文地址:https://blog.csdn.net/weixin_47596351/article/details/138194766