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

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

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

六、高级字符驱动程序操作

1、ioctl

(1)用户空间

Linux下的ioctl()函数

// 接口
       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long cmd, ...);

// ==================================================

// 应用示例
uint16 data16;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
  printf("socket failed\n\r");
}
if (ioctl(fd, SIOCSIFVLAN_PVID_PRI, &data16) < 0) {
  printf("ioctl pvid failed\n\r");
}

(2)系统实现

Linux驱动总结3- unlocked_ioctl和堵塞(waitqueue)读写函数的实现

linux unlocked_ioctl

// 2.6.34
// 从2.6.36 以后的内核已经废弃了 file_operations 中的 ioctl 函数指针,
// 取而代之的是 unlocked_ioctl, 用户空间对应的系统调用没有发生变化。
int (*ioctl) (struct inode *, struct file *, unsigned int cmd, 
        unsigned long arg);
long (*unlocked_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
long (*compat_ioctl) (struct file *, unsigned int cmd, unsigned long arg);        

2、阻塞型 I/O

(1)休眠的简单介绍

  对 Linux 设备驱动程序来讲,让一个进程进入休眠状态很容易。但是,为了将进程以一

种安全的方式进入休眠,我们需要牢记两条规则。

第一条规则是:永远不要在原子上下文中进入休眠。我们已经在第五章介绍过原子操作,而原子上下文就是指下面这种状态:在执行多个步骤时,不能有任何的并发访问。这意味着,对休眠来说,我们的驱动程序不能在拥有自旋锁、seqlock 或者 RCU 锁时休眠。如果我们已经禁止了中断,也不能休眠。在拥有信号量时休眠是合法的,但是必须仔细检查拥有信号量时休眠的代码。如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有信号量而休眠的代码必须很短,并且还要确保拥有信号量并不会阻塞最终会唤醒我们自己的那个进程。


另外一个需要铭记的是:当我们被唤醒时,我们永远无法知道休眠了多长时间,或者休眠期间都发生了些什么事情。我们通常也无法知道是否还有其他进程在同一事件上休眠,这个进程可能会在我们之前被唤醒并将我们等待的资源拿走。这样,我们对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真。


 另外一个相关的问题是,除非我们知道有其他人会在其他地方唤醒我们,否则进程不能休眠。完成唤醒任务的代码还必须能够找到我们的进程,这样才能唤醒休眠的进程。为确保唤醒发生,需整体理解我们的代码,并清楚地知道对每个休眠而言哪些事件序列会结束休眠。能够找到休眠的进程意味着,需要维护一个称为等待队列的数据结构。顾名思义,等待队列就是一个进程链表,共中包含了等待某个特定事件的所有进程。

(2)简单休眠

  当进程休眠时,它将期待某个条件会在未来成为真。我们前面提到,当一个休眠进程被唤醒时,它必须再次检查它所等待的条件的确为真。 Linux 内核中最简单的休眠方式是称为 wait_event 的宏(以及它的几个变种);在实现休眠的同时,它也检查进程等待的条件。wait_event 的形式如下:

wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

 在上面所有的形式中,queue 是等待队列头。注意,它 “通过值” 传递,而不是通过指针。condition 是任意一个布尔表达式,上面的宏在休眠前后都要对该表达式求值;在条件为真之前,进程会保持休眠。注意,该条件可能会被多次求值,因此对该表达式的求值不能带来任何副作用。


 如果使用 wait_event、进程将被置于非中断休眠,如我们先前提到的,这通常不是我们所期望的。最好的选择是使用 wait_event_interruptible,它可以被信号中断。这个版本可返回一个整数值,非零值表示休眠被某个信号中断,而驱动程序也许要返回 -ERESTARTSYS。后面的两个版本(wait_event_timeout 和 wait_event_interruptible_timeout)只会等待限定的时间;当给定的时间(以 jiffy 表示,第七章将讨论)到期时,无论 condition 为何值,这两个宏都会返回 0 值。


 当然,整个过程的另外一半是唤醒。其他的某个执行线程(可能是另一个进程或者中断处理例程)必须为我们执行唤醒、因为我们的进程正在休眠中。用来唤醒休眠进程的基本函数是 wake_up,它也有多种形式,但这里先介绍其中两个:

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible (wait_queue_head_t *queue);

  wake_up 会唤醒等待在给定 queue 上的所有进程(实际情况要复杂一些、读者很快会看到)。另一个形式(wake_up_interruptible)只会唤醒那些执行可中断休眠的进程。通常,这两种形式很难区分(如果使用可中断休眠的话);在实践中、约定作法是在使用 wait_event 时使用 wake_up ,而在使用 wait_event_interruptible 时使用 wake_up_interruptible

3、快速参考

本章介绍了下面这些符号和头文件:

#include <linux/ioctl.h>
// 这个头文件声明了用于定义 ioctl命令的所有的宏。它现在包含在 <linux/fs.h> 中。

_IOC_NRBITS

_IOC_TYPEBITS

_IOC_SIZEBITS

_IOC_DIRBITS

 ioctl 命令的不同位字段的可用位数。还有四个宏定义了不同的 MASK(掩码),另

 外四个宏定义了不同的 SHIFT (偏移),但它们基本上仅在内部使用。由于

 _IOC_SIZEBITS 在不同体系架构上的值不同,因此需要重点关注。

 

_IOC_NONE

_IOC_READ

_IOC_WRITE

 "方向"位字段的可能值。"读"和"写"是不同的位,可以"OR"在一起来指定

 读 / 写。这些值都是基于 0 的。

_IOC(dir,type,nr,size)
_IO(type,nr)
_IOR(type,nr,size)
_IOW(type,nr,size)
_IOWR(type,nr,size)
// 用于生成 ioctl 命令的宏。

_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)
// 用于解码 ioctl命令的宏。特别地,_IOC_TYPE(nr)是_IOC_READ和_IOC_WRITE进行"OR"的结果。
#include <asm/uaccess.h>
int access_ok(int type, const void *addr, unsigned long size);
// 这个函数验证指向用户空间的指针是否可用。如果允许访问,access_ok 返回非零值。

VERIFY_READ

VERIFY_WRITE

  在 access_ok 中 type 参数可取的值。VERIFY_WRITE 是 VERIFY_READ 的超集。

#include <asm/uaccess.h>
int put_user(datum,ptr);
int get_user(local,ptr);
int __put_user(datum,ptr);
int __get_user(local,ptr);
/*
 * 用于向(或从)用户空间保存(或获取)单个数据项的宏。传送的字节数目由
 * sizeof(*ptr) 决定。前两个要先调用 access_ok,后两个(__put_user 和
 * __get_user)则假设 access_ok 已经被调用过了。
*/


#include <linux/capability.h>
// 定义有各种 CAP_符号,用于描述用户空间进程可能拥有的权能操作。
int capable(int capability);
// 如果进程具有指定的权能,返回非零值。


#include <linux/wait.h>
typedef struct { /* */ } wait_queue_head_t;
void init_waitqueue_head(wait_queue_head_t *queue);
DECLARE_WAIT_QUEUE_HEAD(queue);
/*
 * 预先定义的 Linux 等待队列类型。wait_queue_head_t 类型必须显式地初始化,
 * 初始化方法可在运行时用 init_waitqueue_head,或在编译时用 DECLARE_WAIT_QUEUE_HEAD
*/

void wait_event(wait_queue_head_t q, int condition);
int wait_event_interruptible(wait_queue_head_t q, int condition);
int wait_event_timeout(wait_queue_head_t q, int condition, int time);
int wait_event_interruptible_timeout(wait_queue_head_t q, int condition,
                  int time);
// 使进程在指定的队列上休眠,直到给定的 condition值为真。

void wake_up(struct wait_queue **q);
void wake_up_interruptible(struct wait_queue **q);
void wake_up_nr(struct wait_queue **q, int nr);
void wake_up_interruptible_nr(struct wait_queue **q, int nr);
void wake_up_all(struct wait_queue **q);
void wake_up_interruptible_all(struct wait_queue **q);
void wake_up_interruptible_sync(struct wait_queue **q);
/*
 * 这些函数唤醒休眠在队列 q 上的进程。_interruptible形式的函数只能唤醒可中断的
 * 进程。通常,只会唤醒一个独占等待进程,但其行为可通过 _nr 或_all 形式改变。
 * _sync 版本的唤醒函数在返回前不会重新调度 CPU。
*/


#include <linux/sched.h>
set_current_state(int state);
// 设置当前进程的执行状态。TASK_RUNNING 表示准备运行,而休眠状态是
// TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。
void schedule(void);
// 从运行队列中选择一个可运行进程。选定的进程可以是 current 或另一个不同的进程。


typedef struct { /* */ } wait_queue_t;
init_waitqueue_entry(wait_queue_t *entry, struct task_struct *task);
// wait_queue_t 类型用来将某个进程放置到一个等待队列上。

void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait,
                int state);
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
// 可用于手工休眠代码的辅助函数。

void sleep_on(wiat_queue_head_t *queue);
void interruptible_sleep_on(wiat_queue_head_t *queue);
// 已废弃的两个函数,它们将当前进程无条件地置于休眠状态。


#include <linux/poll.h>
void poll_wait(struct file *filp, wait_queue_head_t *q, poll_table *p)
// 将当前进程置于某个等待队列但并不立即调度。该函数主要用于设备驱动程序的 poll 方法。

int fasync_helper(struct inode *inode, struct file *filp, int mode, 
          struct fasync_struct **fa);
// 用来实现 fasync 设备方法的辅助函数。mode 参数取传入该方法的同一值,而 fa
// 指向设备专有的 fasync_struct*。

void kill_fasync(struct fasync_struct *fa, int sig, int band);
// 如果驱动程序支持异步通知,则这个函数可以用来发送一个信号给注册在 fa 中的进程。

int nonseekable_open(struct inode *inode, struct file *filp);
loff_t no_llseek(struct file *file, loff_t offset, int whence);
// 任何不支持定位的设备都应该在其 open方法中调用 nonseekable_open。这类设备
// 还应该在其 llseek 方法中使用 no_llseek。

七、时间、延迟及延缓操作

1、度量时间差

(1)使用 jiffies 计数器

#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_before_eq(unsigned long a, unsigned long b);

  如果 a(jiffies 的某个快照)所代表的时间比 b 靠后,则第一个宏返回真;如果 ab 靠前,则第二个宏返回真;后面两个宏分别用来比较 “靠后或者相等” 及 “靠前或者相等” 。这些宏会将计数器值转换为 signed long,相减,然后比较结果。如果需要以安全的方式计算两个 jiffies 实例之间的差,也可以使用相同的技巧:

diff = (long)t2 - (long)t1;

而通过下面的方法,可将两个 jiffies 的差转换为毫秒值:

msec = diff * 1000 / HZ;

  但是,我们有时需要将来自用户空间的时间表述方法(使用 struct time valstruct timespec)和内核表述方法进行转换。这两个结构使用两个数来表示精确的时间:在老的、流行的 struct timeval 中使用秒和毫秒值,而较新的 struct timespce中则使用秒和纳秒,前者比后者出现得早,但更常用。为了完成 jiffies 值和这些结构间的转换,内核提供了下面四个辅助函数:

#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);

  对 64jiffies_64 的访问不像对 jiffies 的访问那样直接。在 64 位计算机架构上,这两个变量其实是同一个;但在 32 位处理器上,对 64 位值的访问不是原子的。这意味着,在我们读取 64 位值的高 32 位及低 32 位时,可能会发生更新,从而获得错误的值。因此,对 64 位计数器的直接读取是很靠不住的,但如果必须读取 64 位计数器,则应该使用内核导出的一个特殊辅助函数,该函数为我们完成了适当的锁定:

#include <linux/jiffies.h>
u64 get_jiffies_64(void);

  在上面的函数原型中使用了 u64 类型。这是由 定义的类型之一,我们将在第十一章讨论这些类型,它其实代表了一个无符号的 64 位类型。

(2)获取当前时间

  mktime 函数为内核提供的将墙钟时间转换为 jiffies

/* Converts Gregorian date to seconds since 1970-01-01 00:00:00.
 * Assumes input in normal date format, i.e. 1980-12-31 23:59:59
 * => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
 *
 * [For the Julian calendar (which was used in Russia before 1917,
 * Britain & colonies before 1752, anywhere else before 1582,
 * and is still in use by some communities) leave out the
 * -year/100+year/400 terms, and add 10.]
 *
 * This algorithm was first published by Gauss (I think).
 *
 * WARNING: this function will overflow on 2106-02-07 06:28:16 on
 * machines where long is 32-bit! (However, as time_t is signed, we
 * will already get problems at other places on 2038-01-19 03:14:08)
 */
unsigned long
mktime(const unsigned int year0, const unsigned int mon0,
       const unsigned int day, const unsigned int hour,
       const unsigned int min, const unsigned int sec)
{
  unsigned int mon = mon0, year = year0;

  /* 1..12 -> 11,12,1..10 */
  if (0 >= (int) (mon -= 2)) {
    mon += 12;  /* Puts Feb last since it has leap day */
    year -= 1;
  }

  return ((((unsigned long)
      (year/4 - year/100 + year/400 + 367*mon/12 + day) +
      year*365 - 719499
      )*24 + hour /* now have hours */
    )*60 + min /* now have minutes */
  )*60 + sec; /* finally seconds */
}

EXPORT_SYMBOL(mktime);

do_gettimeofday 使用秒或微秒值来填充一个指向 struct timeval 的指针变量。

/**
 * do_gettimeofday - Returns the time of day in a timeval
 * @tv:   pointer to the timeval to be set
 *
 * NOTE: Users should be converted to using getnstimeofday()
 */
void do_gettimeofday(struct timeval *tv)
{
  struct timespec now;

  getnstimeofday(&now);
  tv->tv_sec = now.tv_sec;
  tv->tv_usec = now.tv_nsec/1000;
}

EXPORT_SYMBOL(do_gettimeofday);

2、延迟执行

(1)长延迟

(a)忙等待
while (time_before(jiffies, j1))
  cpu_relax();
(b)让出处理器
while (time_before(jiffies, j1))
  schedule();
(c)超时
// include/linux/wait.h
#define wait_event_timeout(wq, condition, timeout)      \
({                  \
  long __ret = timeout;           \
  if (!(condition))             \
    __wait_event_timeout(wq, condition, __ret);   \
  __ret;                \
})

#define __wait_event_interruptible(wq, condition, ret)      \
do {                  \
  DEFINE_WAIT(__wait);            \
                  \
  for (;;) {              \
    prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);  \
    if (condition)            \
      break;            \
    if (!signal_pending(current)) {       \
      schedule();         \
      continue;         \
    }             \
    ret = -ERESTARTSYS;         \
    break;              \
  }               \
  finish_wait(&wq, &__wait);          \
} while (0)
// include/linux/sched.h
extern signed long schedule_timeout(signed long timeout);
extern signed long schedule_timeout_interruptible(signed long timeout);
extern signed long schedule_timeout_killable(signed long timeout);
extern signed long schedule_timeout_uninterruptible(signed long timeout);

(2)短延迟

  当设备驱动程序需要处理硬件的延迟(latency)时,这种延迟通常最多涉及到几十个毫秒。在这种情况下,依赖于时钟滴答显然不是正确的方法。

  ndelayudelaymdelay 这几个内核函数可很好完成短延迟任务,它们分别延迟指定数量的纳秒、微秒和毫秒时间。它们的原型如下:

#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

// arch/avr32/lib/delay.c
inline void __const_udelay(unsigned long xloops)
{
  unsigned long long loops;

  asm("mulu.d %0, %1, %2"
      : "=r"(loops)
      : "r"(current_cpu_data.loops_per_jiffy * HZ), "r"(xloops));
  __delay(loops >> 32);
}

void __udelay(unsigned long usecs)
{
  __const_udelay(usecs * 0x000010c7); /* 2**32 / 1000000 (rounded up) */
}

void __ndelay(unsigned long nsecs)
{
  __const_udelay(nsecs * 0x00005); /* 2**32 / 1000000000 (rounded up) */
}

// arch/xtensa/include/asm/delay.h
static inline void __delay(unsigned long loops)
{
  /* 2 cycles per loop. */
  __asm__ __volatile__ ("1: addi %0, %0, -2; bgeui %0, 2, 1b"
      : "=r" (loops) : "0" (loops));
}

 这些函数的实际实现包含在  中,其实现和具体的体系架构相关,有时构建于一个外部函数。所有的体系架构都会实现 udelay,但其他函数可能未被定义;如果存在没有真正定义的函数,则  会在 udelay 的基础上提供一个默认的版本。不管哪种情况,真正实现的延迟至少会达到所请求的时间值,但可能更长;实际上,当前所有平台都无法达到纳秒精度,但有些平台提供了子微秒精度。延迟超过请求的值通常不是问题,因为驱动程序的短延迟通常等待的是硬件,而需求往往是至少要等待给定的时间段。


 udelay(以及可能的 ndelay)的实现使用了软件循环,它根据引导期间计算出的处理器速度以及 loops_pre_jiffy 整数变量确定循环的次数。如果读者要阅读实际的代码,要注意 x86 平台上的实现相当复杂,这是因为,它使用了不同的定时源,而这取决于运行代码的 CPU 类型。


 为避免循环计算中的整数溢出,udelay 和 ndelay 为传递给它们的值强加了上限。如果模块无法装载、并显示未解析的符号 __bad_udelay,则说明模块在调用 udelay 时传入了太大的值。但需要注意的是,这种编译时的检查只能在常量值上进行,而且并不是所有的平台都实现了这种检查。作为一般性的规则,如果我们打算延迟上千个纳秒,则应该使用 udelay 而不是 ndelay; 类似地,毫秒级的延迟也应该利用 mdelay 而不是更细粒度的短延迟函数。


 要重点记住的是,这三个延迟函数均是忙等待函数,因而在延迟过程中无法运行其他任务。这样,这些函数将重复 jitbusy 的行为,只是在不同的量级上。因此,我们应该只在没有其他实用方法时使用这些函数。


 实现毫秒级(或者更长)延迟还有另一种方法,这种方法不涉及忙等待。 文件声明了下面这些函数:

void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)

 前两个函数将调用进程休眠以给定的 millisecs。对 msleep 的调用是不可中断的;我们可以确信进程将至少休眠给定的毫秒数。如果驱动程序正在某个等待队列上等待,而又希望有唤醒能够打断这个等待的话,则可使用 msleep_interruptible。msleep_interruptible 的返回值通常是零;但是,如果进程被提前唤醒,那么返回值就是原先请求休眠时间的剩余毫秒数。对 ssleep 的调用将使进程进入不可中断的休眠,但休眠时间以秒计。

  通常,如果我们能够容忍比所请求更长的延迟,则应当使用 chedule_timeoutsleep 或者 ssleep

3、内核定时器

 如果我们需要在将来的某个时间点调度执行某个动作,同时在该时间点到达之前不会阻塞当前进程,则可以使用内核定时器。内核定时器可用来在未来的某个特定时间点(基于时钟滴答)调度执行某个函数,从而可用于完成许多任务;例如,如果硬件无法产生中断,则可以周期性地轮询设备状态。另一个内核定时器的典型应用是关闭软驱马达,或者结束其他长时间的关闭操作。在这种情况下,在 close 方法返回前进行延迟将会给应用程序带来不必要的(甚至令人惊讶的)开销。最后,内核本身也在许多情况下使用了定时器,包括在 schedule_timeout 的实现中。


 一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的通数。其实现位于  和 kernel/timer.c 文件,我们将在 “内核定时器的实现” 一节中对此进行详细描述。


 被调度运行的函数几乎肯定不会在注册这些函数的进程正在执行时运行。相反,这些函数会异步地运行。到此为止,我们提供的示例驱动程序代码都在进程执行系统调用的上下文中运行。但是,当定时器运行时,调度该定时器的进程可能正在休眠或在其他处理器上执行,成干脆已经退出。


 这种异步执行类似于硬件中断发生时的情景(我们会在第十章详细讨论)。实际上,内核定时器常常是作为 “软件中断” 的结果而运行的。在这种原子性的上下文中运行时,代码会受到许多限制。定时器函数必须以我们在第五章 “自旋锁和原子上下文” 一节中讨论的方式原子地运行,但是这种非进程上下文还带来其他一些问题。现在我们要讨论这些限制,这些限制还会在本书后面多次出现。我们也会对此多次重复,原子上下文中的这些规则必须遵守,否则会导致大麻烦。


 许多动作需要在进程上下文中才能执行。如果处于进程上下文之外(比如在中断上下文中),则必须遵守如下规则:


不允许访问用户空间。因为没有进程上下文,无法将任何特定进程与用户空间关联起来。

current 指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何关联。

不能执行休眠或调度。原子代码不可以调用 schedule 或者 wait_event,也不能调用任何可能引起休眠的函数。例如,调用 kmaIloc(…,GFP_KERNEL) 就不符合本规则。信号量也不能用,因为可能引起休眠。

(1)定时器 API

// include/linux/timer.h
struct timer_list {
  /* ... */
  unsigned long expires;
  void (*function)(unsigned long);
  unsigned long data;
};

void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);

void add_timer(struct timer_list *timer);
int del_timer(struct timer_list *timer);

 上面给出的数据结构其实包含其他一些未列出的字段,但给出的三个字段是可由定时器代码以外的代码访问。expires 字段表示期望定时器执行的 jiffies 值;到达该 jiffies 值时,将调用 function 函数,并传递 data 作为参数。如果需要通过这个参数传递多个数据项,那么可以将这些数据项据绑成一个数据结构,然后将该数据结构的指针强制转换成 unsigned long 传入。这种技巧在所有内核支持的体系架构上都是安全的,而且在内存管理(参见第十五章的讨论)中非常常见。expires 的值并不是 jiffies_64 项,这是因为定时器并不适用于长的未来时间点,而且 32 位平台上的 64 位操作会比较慢。

  除了上面给出的函数及接口以外,内核定时器 API 还包括其他几个函数。下面给出这些

函数的完整描述:

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

更新某个定时器的到期时间,经常用于超时定时器(典型的例子是软驱的关马达定

时器)。我们也可以在通常使用 add_timer 的时候在不活动的定时器上调用 mod_timer。

int del_timer_sync(struct timer_list *timer);

和 del_timer 的工作类似,但该函数可确保在返回时没有任何 CPU 在运行定时器函

数。del_timer_sync 可用于在 SMP 系统上避免竞态,这和单处理器内核中的

del_timer 是一样的。在大多数情况下,应优先考虑调用这个函数而不是 del_timer

函数。如果从非原子上下文调用,该函数可能休眠,但在其他情况下会进入忙等待。

在拥有锁时,应格外小心调用 del_timer_sync,因为如果定时器函数企图获取相同

的锁,系统就会进入死锁。如果定时器函数会重新注册自己,则调用者必须首先确

保不会发生重新注册;这通常通过设置一个由定时器函数检查的 “关闭” 标志来实现。

int timer_pending(const struct timer_list * timer);

该函数通过读取 timer_list 结构的一个不可见字段来返回定时器是否正在被调度运行。

(2)内核定时器的实现

  可参考 ==> (2)软定时器和延迟函数

 尽管要使用内核定时器并不必知道它们的具体实现,但其实现非常有意思,而了解其内部也是值得的。

 内核定时器的实现要满足如下需求及假定:


定时器的管理必须尽可能做到轻量级。

其设计必须在活动定时器大量增加时具有很好的伸缩性。

大部分定时器会在最多几秒或者几分钟内到期,而很少存在长期延迟的定时器。

定时器应该在注册它的同一 CPU 上运行。

 内核开发发者使用的解决方案是利用 Per-CPU 数据结构。timer_list 结构的 base 字段包含了指向该结构的指针。如果 base 为 NULL、定时器尚未调度运行;否则,该指针会告诉我们哪个数据结构(也就是哪个 CPU)在运行定时器。Per-CPU 数据项在第八章的 “Per-CPU 变量” 一节中描述。


 不管何时内核代码注册了一个定时器(通过 add_timer 或者 mod_timer),其操作最终会由 internal_add_timer(定义在 kernel/timer.c 中)执行,该函数又会将新的定时器添加到和当前 CPU 关联的 “级联表” 中的定时器双向链表中。


 级联表的工作方式如下: 如果定时器在接下来的 0 ~ 255 个 jiffies 中到期,则该定时器就会被添加到 256 个链表中的一个(这取决于 expires 字段的低 8 位值),这些链表专用于短期定时器。如果定时器会在较远的未来到期(但在 16384 个 jiffies 之前),则该定时器会被添加到 64 个链表之一(这取决于 expires 字段的 9 ~ 14 位,共 6 位,值为 64)。对更远将来的定时器,相同的技巧用于 15 ~ 20 位(从 1 开始 ?)、21 ~ 26 位以及 27 - 31 位。如果定时器的 expires 字段代表了更远的未来(只可能发生在 64 位系统上),则利用延迟值 0xfffffff 做散列(hash)运算,而在过去时间内到期的定时器会在下一个定时器滴答时被调度(在高负荷的情况下,有可能注册一个已经到期的定时器,尤其在运行抢占式内核时)。


 当 __run_timers 被激发时,它会执行当前定时器滴答上的所有挂起的定时器。如果 jiffies 当前是 256 的倍数,该函数还会将下一级定时器链表重新散列到 256 个短期链表中,同时还可能根据上面 jiffies 的位划分对将其他级别的定时器做级联处理。


 这种方法虽然初看起来有些复杂,但能很好地处理定时器不多或有大量定时器的情况。用来管理每个活动定时器所需的必要时间和已注册的定时器数量无关,同时被限于定时器 expires 字段二进制表达上的几个逻辑操作。这种实现唯一的开销在于 512 个链表头(256 个短期链表以及 4 组 64 个的长期链表)占用了 4KB 的存储空间。


 如同 /proc/jitimer 所描述的,函数 __run_timers 运行在原子上下文中。除了我们已经描述过的限制外,这带来了一个有趣的特点:定时器会在正确的时间到期,即使我们运行的不是抢占式的内核,而 CPU 会忙于内核空间。如果读者在后台读取 /proc/jitbusy 而在前台读取 /etc/jitimer 时,就能看到这个特点。尽管系统似乎被忙等待系统调用整个锁住,但内核定时器仍然可很好地工作。


 但需要谨记的是,内核定时器离完美还有很大距离,因为它受到 jitter 以及由硬件中断、其他定时器和异步任务所产生的影响。和简单数字 I/O 关联的定时器对简单任务来说足够了,比如控制步进电机或者业余电子设备,但通常不适合于工业环境下的生产系统。对这类任务,我们需要借助某种实时的内核扩展。

// kernel/timer.c
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)

static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
{
  unsigned long expires = timer->expires;
  unsigned long idx = expires - base->timer_jiffies;
  struct list_head *vec;

  if (idx < TVR_SIZE) {
    int i = expires & TVR_MASK;
    vec = base->tv1.vec + i;
  } else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
    int i = (expires >> TVR_BITS) & TVN_MASK;
    vec = base->tv2.vec + i;
  } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
    int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
    vec = base->tv3.vec + i;
  } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
    int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
    vec = base->tv4.vec + i;
  } else if ((signed long) idx < 0) {
    /*
     * Can happen if you add a timer with expires == jiffies,
     * or you set a timer to go off in the past
     */
    vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);
  } else {
    int i;
    /* If the timeout is larger than 0xffffffff on 64-bit
     * architectures then we use the maximum timeout:
     */
    if (idx > 0xffffffffUL) {
      idx = 0xffffffffUL;
      expires = idx + base->timer_jiffies;
    }
    i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
    vec = base->tv5.vec + i;
  }
  /*
   * Timers are FIFO:
   */
  list_add_tail(&timer->entry, vec);
}

4、tasklet

  和定时问题相关的另一个内核设施是 tasklet(小任务)机制。中断管理(第十章将进一步描述)中大量使用了这种机制。


 tasklet 在很多方面类似内核定时器:它们始终在中断期间运行,始终会在调度它们的同一 CPU 上运行,而且都接收一个 unsigned long 参数。和内核定时器不同的是,我们不能要求 tasklet 在某个给定时间执行。调度一个 tasklet,表明我们只是希望内核选择某个其后的时间来执行给定的函数。这种行为对中断处理例程来说尤其有用,中断处理例程必须可能快地管理硬件中断,而大部分数据管理则可以安全地延迟到其后的时间。实际上,和内核定时器类似,tasklet 也会在 “软件中断” 上下文以原子模式执行。软件中断是打开硬件中断的同时执行某些异步任务的一种内核机制。


 tasklet 以数据结构的形式存在,并在使用前必须初始化。调用特定的函数或者使用特定的宏来声明该结构,即可完成 tasklet 的初始化:

#include <linux/interrupt.h>
struct tasklet_struct {
  /* ... */
  void (*func) (unsigned long);
  unsigned long data;
);

void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);

tasklet 为我们提供了许多有意思的特性:


一个 tasklet 可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet 才会被执行。

和定时器类似,tasklet 可以注册自己本身。

tasklet 可被调度以在通常的优先级或者高优先级执行。高优先级的 tasklet 总会首先执行

如果系统负荷不重,则 tasklet 会立即得到执行,但始终不会晚于下一个定时器滴答。

一个 tasklet 可以和其他 tasklet 并发,但对自身来讲是严格串行处理的,也就是说,同一 tasklet 永远不会在多个处理器上同时运行。当然我们已经指出,tasklet 始终会在调度自己的同一 CPU 上运行。

 用来实现 /proc/jitasklet 和 /proc/jitasklethi 的 jit 模块代码几乎和实现 /proc/jitimer 的代码一模一样,只是前者使用了 tasklet 调用而不是定时器接口。下面的清单描述了tasklet 相关的内核接口,可在 tasklet 结构被初始化之后使用:

void tasklet_disable(struct tasklet_struct *t);

这个函数禁用指定的 tasklet。该 tasklet 仍然可以用 tasklet_schedule 调度,但其执

行被推迟,直到该 tasklet 被重新启用。如果 tasklet 当前正在运行,该函数会进入

忙等待直到 tasklet退出为止;因此,在调用 tasklet_disable 之后,我们可以确信该

tasklet 不会在系统中的任何地方运行。

void tasklet_disable_nosync(struct tasklet_struct *t);

禁用指定的 tasklet,但不会等待任何正在运行的 tasklet 退出。该函数返回后,tasklet

是禁用的,而且在重新启用之前,不会再次被调度。但是,当该函数返回时,指定

的 tasklet 可能仍在其他 CPU 上运行。

void tasklet_enable(struct tasklet_struct *t);

启用一个先前被禁用的 tasklet。如果该 tastlet 已经被调度,它很快就会运行。对

tasklet_enable 的调用必须和每个对 tasklet_disable 的调用匹配,因为内核对每个

tasklet 保存有一个 “禁用计数”。

void tasklet_schedule(struct tasklet_struct *t);

调度执行指定的 tasklet。如果在获得运行机会之前,某个 tasklet 被再次调度,则该

tasklet 只会运行一次。但是如果在该 tasklet 运行时被调度,就会在完成后再次运

行。这样,可确保正在处理事件时发生的其他事件也会被接收并注意到。这种行为

也允许 tasklet 重新调度自身。

void tasklet_hi_schedule(struct tasklet_struct *t);

调度指定的 tasklet 以高优先级执行。当软件中断处理例程运行时,它会在处理其他

软件中断任务(包括 “通常” 的 tasklet)之前处理高优先级的 tasklet。理想状态下,

只有具备低延迟需求的任务(比如填充音频缓冲区)才能使用这个函数,这样可避

免由其他软件中断处理例程引入的额外延迟。和 /proc/jitasklet 相比,/proc/jitasklethi

给出了肉眼能察觉的区别。

void tasklet_kill(struct tasklet_struct *t);

该函数确保指定的 tasklet 不会被再次调度运行;当设备要被关闭或者模块要被移除

时,我们通常调用这个函数。如果 tasklet 正被调度执行,该函数会等待其退出。如

果 tasklet 重新调度自己,则应该避免在调用 tasklet_kill 之前完成重新调度,这和

del_timer_sync 的处理类似。


 tasklet 的实现在 kernel/softirq.c 中。其中有两个(通常优先级和高优先级的)tasklet 链表,它们作为 per-CPU 数据结构而声明,并且使用了类似内核定时器那样的 CPU 相关机制。tasklet 管理中使用的数据结构是个简单的链表,因为 tasklet 不必像内核定时器那样来处理时间问题。

5、工作队列

 从表面看来,工作队列(workqueue)类似于 tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。但是,两者之间存在一些非常重要的区别,其中包括:


tasklet 在软件中断上下文中运行,因此,所有的 tasklet 代码都必须是原子的。相反,工作队列函数在一个特殊内核进程的上下文中运行,因此它们具有更好的灵活性。尤其是,工作队列函数可以休眠。

tasklet 始终运行在被初始提交的同一处理器上,但这只是工作队列的默认方式。

内核代码可以请求工作队列函数的执行延迟给定的时间间隔。

 两者的关键区别在于:tasklet 会在很短的时间段内很快执行,并且以原子模式执行、而工作队列函数可具有更长的延迟并且不必原子化。两种机制有各自适合的情形。


 工作队列有 struct workqueue_struct 的类型,该结构定义在  中。在使用之前,我们必须显式地创建一个工作队列,可使用下面两个函数之一:

struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);

 每个工作队列有一个或多个专用的进程(“内核线程”),这些进程运行提交到该队列的函数。如果我们使用 create_workqueue,则内核会在系统中的每个处理器上为该工作队列创建专用的线程。在许多情况下,众多的线程可能对性能具有某种程度的杀伤力;因此,如果单个工作线程足够使用,那么应该使用 create_singlethread_workqueue 创建工作队列。


 要向一个工作队列提交一个任务,需要填充一个 work_struct 结构,这可通过下面的

宏在编译时完成:

// include/linux/workqueue.h
struct work_struct {
  atomic_long_t data;
#define WORK_STRUCT_PENDING 0   /* T if work item pending execution */
#define WORK_STRUCT_STATIC  1   /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
  struct list_head entry;
  work_func_t func;
#ifdef CONFIG_LOCKDEP
  struct lockdep_map lockdep_map;
#endif
};

DECLARE_WORK(name, void (*function) (void *), void *data);

 其中,name 是要声明的结构名称,function 是要从工作队列中调用的函数,而 data 是要传递给该函数的值。如果要在运行时构造 work_struct 结构,可使用下面两个宏:

INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function) (void *), void *data);

 INIT_WORK 完成更加彻底的结构初始化工作;在首次构造该结构时,应该使用这个宏。 PREPARE_WORK 完成几乎相同的工作,但它不会初始化用来将 work_struct 结构链接到工作队列的指针。如果结构已经被提交到工作队列,而只是需要修改该结构,则应该使用 PREPARE_WORK 而不是 INIT_WORK。


 如果要将工作提交到工作队列,则可使用下面的两个函数之一:

int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue,
             struct work_struct *work, unsignedlong delay);

 它们都会将 work 添加到给定的 queue 。但是如果使用 queue_delayed_work,则实际的工作至少会在经过指定的 jiffies(由 delay 指定)之后才会被执行。如果工作被成功添加到队列,则上述函数的返回值为 1。返回值为非零时意味着给定的 work_struct 结构已经等待在该队列中、从而不能两次加入该队列。


 在将来的某个时间,工作函数会被调用,并传入给定的 data 值。该函数会在工作线程的上下文运行,因此如果必要,它可以休眠 —— 当然,我们应该仔细考虑休眠会不会影响提交到同一工作队列的其他任务。但是该函数不能访问用户空间,这是因为它运行在内核线程,而该线程没有对应的用户空间可以访问。

  如果要取消某个挂起的工作队列入口项,可调用:

int cancel_delayed_work(struct work_struct *work);

 如果该入口项在开始执行前被取消,则上述函数返回非零值。在调用 cancel_delayed_work 之后,内核会确保不会初始化给定入口项的执行。但是,如 cancel_delayed_work 返回 0,则说明该入口项已经在其他处理器上运行,因此在 cancel_delayed_work 返回后可能仍在运行。为了绝对确保在 cancel_delayed_work 返回 0 之后,工作函数不会在系统中的任何地方运行,则应该随后调用下面的函数:

void flush_workqueue(struct workqueue_struct *queue);

 在 flush_workqueue 返回后,任何在该调用之前被提交的工作函数都不会在系统任何地方运行。


 在结束对工作队列的使用后,可调用下面的函数释放相关资源:

void destroy_workqueue(struct workqueue_struct *queue);

(1)共享队列

 在许多情况下,设备驱动程序不需要有自己的工作队列。如果我们只是偶尔需要向队列中提交任务,则一种更简单、更有效的办法是使用内核提供的共享的默认工作队列。但是,如果我们使用这个工作队列,则应该记住我们正在和其他人共享该工作队列。这意味着,我们不应该长期独占该队列,即不能长时间休眠,而且我们的任务可能需要更长的时间才能获得处理器时间。


 如果需要取消已提交到共享队列中的工作入口项,则可使用上面描述过的 cancel_delayed_work 函数。但是,刷新共享工作队列时需要另一个函数:

void flush_scheduled_work(void);

  因为我们无法知道其他人是否在使用该队列,因此我们也无法知道在 flush_scheduled_work 返回前到底要花费多少时间。

6、快速参考

  本章引入了如下符号:

(1)计时

#include <linux/param.h>

HZ

Hz 符号指出每秒钟产生的时钟滴答数。

#include <linux/jiffies.h>
volatile unsigned long jiffies
u64 jiffies_64

jiffies_64 变量会在每个时钟滴答递增,也就是说,它会在每秒递增 HZ 次。内

核代码大部分情况下使用 jiffies,在 64 位平台上,它和 jiffies_64 是一样的,

而在 32 位平台上,jiffies 是 jiffies_64 的低 32 位。

int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigmed long b) ;
int time_before_eq(unsigned long a, unsigned long b);

这些布尔表达式以安全方式比较 jiffies,无需考虑计数器溢出的问题,也不必访问 jiffies_64。

u64 get_jiffies_64(void);
// 无竞态地获取 jiffies_64 的值。

#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
// 在 jiffies 表示的时间和其他表示法之间转换。
#include <asm/msr.h>
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var32);

x86 专用的宏,用来读取时间截计数器。上述宏用两个 32 位字的形式读取该计数

器,要么读取低 32 位,要么整个读取到一个 long long 型的变量中。

#include <linux/timex.h>
cycles_t get_cycles(void);
// 以平台无关的方式返回时间戳计数器。如果 CPU 不提供时间戳特性,则返回 0。

#include <linux/time.h>
unsigned long mktime(year, mon, day, h, m, s);
// 根据 6 个无符号的 int 参数返回自 Epoch 以来的秒数。

void do gettimeofday(struct timeval *tv);
// 以自 Epoch 以来的秒数和毫秒数的形式返回当前时间,并且以硬件能提供的最好分
// 辨率返回。在大多数平台上,分辨率是微秒或更好的级别,但某些平台只能提供 jiffies 级
// 的分辨率。

struct timespec current_kernel_time(void);
// 以 jiffies 为分辨率返回当前时间。

(2)延迟

#include <linux/wait.h>
long wait_event_interruptible_timeout(wait_queue_head_t *q, condition, signed long timeout);
// 使当前进程休眠在等待队列上,并指定用 jiffies 表达的超时值。如果要进入不可中
// 断休眠,则应使用 schedule_timeout(见下)。

#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
// 调用调度器,确保当前进程可在给定的超时值之后被唤醒。调用者必须首先调用
// set_current_state 将自己置于可中断或不可中断的休眠状态。

#include <linux/delay.h>
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);
// 使进程休眠给定的毫秒数(或使用 ssleep 休眠给定的秒数)。

(3)内核定时器

#include <asm/hardirq.h>
int in_interrupt(void);
int in_atomic(void);
// 返回布尔值以告知调用代码是否在中断上下文或者在原子上下文中执行。中断上下
// 文在进程上下文之外,可能正处理硬件或软件中断。原子上下文是指不能进行调度
// 的时间点,比如中断上下文或者拥有自旋锁时的进程上下文。

#include <linux/timer.h>
void init_timer(struct timer_list * timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
// 上面的函数以及静态声明定时器结构的宏是初始化 timer_list 数据结构的两种方式。

void add_timer(struct timer_list * timer);
// 注册定时器结构,以在当前 CPU 上运行。
int mod_timer(struct timer_list *timer, unsigned long expires);
// 修改一个已经调度的定时器结构的到期时间。它也可以替代 add_timer 函数使用。
int timer_pending(struct timer_list * timer);
// 返回布尔值的宏、用来判断给定的定时器结构是否已经被注册运行。
void del_timer(struct timer_list * timer);
void del_timer_sync(struct timer_list * timer);
// 从活动定时器清单中删除一个定时器。后一个函数确保定时器不会在其他 CPU 上运行。

(4)tasklet

#include <linux/interrupt.h>
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
void tasklet_init(struct tasklet_struct *t, void (*func) (unsigned long),
          unsigned long data);
// 前面两个宏声明一个 tasklet 结构,而 tasklet_init 函数初始化一个通过分配或者其
// 他途径获得的 tasklet 结构。第二个 DESCLARE 宏禁用给定的 tasklet。

void tasklet_disable(struct tasklet_struct *t);
void tasklet_disable_nosync(struct tasklet_struct *t);
void tasklet_enable(struct tasklet_struct *t);
// 禁用或重新启用某个 tasklet。每次禁止都要匹配一次使能(我们可以禁用一个已经
// 被禁用的 tasklet)。tasklet_disable 函数会在 tasklet 正在其他 CPU 上运行时等待,
// 而 nosync 版本不会完成这个额外的步骤。

void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
// 调度运行某个 tasklet,可以是"通常"的 tasklet或者一个高优先级的 tasklet。当执
// 行软件中断时,高优先级的 tasklet 会被首先处理,而通常的 tasklet 最后运行。

void tasklet_kill(struct tasklet_struct *t);
// 如果指定的 tasklet 被调度运行,则将其从活动链表中删除。和 tasklet_disable 类
// 似,该函数可在 SMP 系统上阻塞,以便等待正在其他 CPU 上运行的该 tasklet 终止。

(5)工作队列

#include <linux/workqueue.h>
struct workqueue_struct;
struct work_struct;
// 上述结构分别表示工作队列和工作入口项。

struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
void destroy_workqueue(struct workqueue_struct *queue);
// 用于创建和销毁工作队列的函数。调用 create_workqueue 将创建一个队列,且系统
// 中的每个处理器上都会运行一个工作线程;相反,create_singlethread_workqueue
// 只会创建单个工作进程。

DECLARE_WORK(name, void (*function)(void *), void *data);
INIT_WORK(struct work_struct *work, void (*furction)(void *), void *data);
PREPARE_WORK(struct work_struct *work, vo.d (*function) (void *),void *data);
// 用于声明和初始化工作队列入口项的宏。

int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct
            work_struct *work, unsigned long delay);
// 用来安排工作以便从工作队列中执行的函数。

int cancel_delayed_work(struct work_struct *work);
void flush_workqueue(struct workqueue_struct *queue);
// 使用 cancel_delayed_work 可从工作队列中删除一个入口项; flush_workqueue 确保
// 系统中任何地方都不会运行任何工作队列入口项。

int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
void flush_scheduled_work(void);
// 使用共享工作队列的函数。

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

目录
相关文章
|
3月前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
45 6
|
4月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
50 5
|
4月前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
41 3
|
4月前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
34 3
|
4月前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
24 1
|
4月前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
40 1
|
4月前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
30 1
|
4月前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
43 1
|
4月前
|
存储 缓存 安全
Linux 设备驱动程序(三)(下)
Linux 设备驱动程序(三)
33 0
下一篇
无影云桌面