Linux内核——字符设备驱动

本文详细介绍了Linux内核中的字符设备驱动框架,包括设备节点、设备号、cdev对象、file_operations结构体的使用,以及open、read、write、ioctl等操作的处理。同时,展示了如何注册字符设备、动态申请设备号、创建设备节点,并通过mdev创建设备文件。最后,提到了如何通过cdev结构与用户空间进行交互,以及驱动安全性和性能优化的方法。
摘要由CSDN通过智能技术生成

字符设备驱动的框架


设备节点:inode,类型为字符设备,记录设备号
设备号:内核确定驱动的唯一编号
cdev:字符驱动对象

框架代码

驱动

#include <linux/module.h>
#include <linux/file.h>
#include <linux/rtc.h>

static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
{
        printk("%s\n", __func__);
        return 0;
}

static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
{
        printk("%s\n", __func__);
        return 0;
}

static int rtc_open (struct inode *pinode, struct file *fp)
{
        printk("%s\n", __func__);
        return 0;
}

static int rtc_release (struct inode *pinode, struct file *fp)
{
        printk("%s\n", __func__);
        return 0;
}

static const struct file_operations rtc_fops = {
        .owner = THIS_MODULE,
        .read = rtc_read,
        .write = rtc_write,
        .open = rtc_open,
        .release = rtc_release,
};

static int __init rtc_init(void)
{
        if (register_chrdev(222, "rtc-demo", &rtc_fops) < 0) {
                printk("failed to register_chrdev\n");
                return -1;
        }

        return 0;
}

static void __exit rtc_exit(void)
{
}

module_init(rtc_init);
module_exit(rtc_exit);

MODULE_LICENSE("GPL");

测试应用

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct rtc_time {
        unsigned int year;
        unsigned int mon;
        unsigned int day;
        unsigned int hour;
        unsigned int min;
        unsigned int sec;
};

int main()
{
        int fd;
        struct rtc_time tm;

        if ((fd = open("/dev/rtc-demo", O_RDWR)) < 0) {
                perror("open");
                return -1;
        }

        if (read(fd, &tm, sizeof(tm)) < sizeof(tm)) {
                perror("read");
                return -1;
        }

        printf("%d:%d:%d\n", tm.hour, tm.min, tm.sec);

        close(fd);

        return 0;
}

运行前构造inode

mknod /dev/rtc-demo c 222 0

register_chrdev 分析

注册 字符设备的关键是将 cdev 放到一个容器中,待后续使用。

注册设备驱动需要的操作

  • 申请设备号
  • 构建 cdev 结构
  • 使用设备号作为键,将cdev放到 kobj_map 容器中。
  int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  {
      p->dev = dev;
      p->count = count;
      return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
  }

  struct kobj_map {
      struct probe {
          struct probe *next;
          dev_t dev;
          unsigned long range;
          struct module *owner;
          kobj_probe_t *get;
          int (*lock)(dev_t, void *);
          void *data;
      } *probes[255];
      struct mutex *lock;
  };

  int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
           struct module *module, kobj_probe_t *probe,
           int (*lock)(dev_t, void *), void *data)
  {
      unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;  // 次设备号获得创建设备数量
      unsigned index = MAJOR(dev);      // 使用 主设备号做哈希键
      unsigned i;
      struct probe *p;

      if (n > 255)
          n = 255;

      p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);

      if (p == NULL)
          return -ENOMEM;

      for (i = 0; i < n; i++, p++) {        // 每个设备的probe复制
          p->owner = module;
          p->get = probe;
          p->lock = lock;
          p->dev = dev;
          p->range = range;
          p->data = data;      // 这里data 为 cdev
      }
      mutex_lock(domain->lock);
      for (i = 0, p -= n /* 把p复位,指向要加入的首个probe */; i < n; i++, p++, index++) {
          struct probe **s = &domain->probes[index % 255];  // probes是元素为指针的数组,s指向数组元素
          while (*s && (*s)->range < range)
              s = &(*s)->next;
          p->next = *s;    // 加入链表
          *s = p;
      }
      mutex_unlock(domain->lock);
      return 0;
  }

注册完cdev后,cdev加入cdev_map表待使用

open分析

  • 根据pathname找到 inode
  • 根据inode的i_flag知道是设备文件,并使用 inode->i_rdev找到驱动
  • 将cdev->ops复制给 file->f_op,返回file
    open后,file->f_op就关联了驱动代码
    具体看如何通过 设备号找到 cdev 和 fops
  int chrdev_open(struct inode * inode, struct file * filp)
  {
      struct cdev *p;
      struct cdev *new = NULL;
      int ret = 0;

      spin_lock(&cdev_lock);
      p = inode->i_cdev;   // 第一次open时,p == NULL
      if (!p) {  // 第一次open
          struct kobject *kobj;
          int idx;
          spin_unlock(&cdev_lock);
          kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);  // 根据设备号找 kobj
          if (!kobj)
              return -ENXIO;
          new = container_of(kobj, struct cdev, kobj);  // struct cdev 包含 kobj 属性,偏移以获得 cdev
          spin_lock(&cdev_lock);
          p = inode->i_cdev;
          if (!p) {    
              inode->i_cdev = p = new;
              inode->i_cindex = idx;
              list_add(&inode->i_devices, &p->list);
              new = NULL;
          } else if (!cdev_get(p))
              ret = -ENXIO;
      } else if (!cdev_get(p))
          ret = -ENXIO;
      spin_unlock(&cdev_lock);
      cdev_put(new);
      if (ret)
          return ret;
      filp->f_op = fops_get(p->ops);   // 根据cdev->ops 获得 fops 记录到 file
      if (!filp->f_op) {
          cdev_put(p);
          return -ENXIO;
      }
      if (filp->f_op->open) {
          lock_kernel();
          ret = filp->f_op->open(inode,filp);  // 回调 具体驱动的 open
          unlock_kernel();
      }
      if (ret)
          cdev_put(p);
      return ret;
  }

根据 设备号获得 kobj

  struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
  {
      struct kobject *kobj;
      struct probe *p;
      unsigned long best = ~0UL;

  retry:
      mutex_lock(domain->lock);
      for (p = domain->probes[MAJOR(dev) % 255] /* 以主设备号做哈希键 */; p; p = p->next) {
          struct kobject *(*probe)(dev_t, int *, void *);
          struct module *owner;
          void *data;

          if (p->dev > dev || p->dev + p->range - 1 < dev)
              continue;
          if (p->range - 1 >= best)
              break;
          if (!try_module_get(p->owner))
              continue;
          owner = p->owner;
          data = p->data;
          probe = p->get;
          best = p->range - 1;
          *index = dev - p->dev;
          if (p->lock && p->lock(dev, data) < 0) {
              module_put(owner);
              continue;
          }
          mutex_unlock(domain->lock);
          kobj = probe(dev, index, data);
          /* Currently ->owner protects _only_ ->probe() itself. */
          module_put(owner);
          if (kobj)
              return kobj;  // return
          goto retry;
      }
      mutex_unlock(domain->lock);
      return NULL;
  }

read分析

可见由于open后,file->f_op 为 cdev->fops 所以,基于文件的操作都通过file->f_op就能调用驱动。

另一种写法

#include <linux/module.h>
#include <linux/file.h>
#include <linux/rtc.h>

static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
{
        printk("%s\n", __func__);
        return 0;
}

static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
{
        printk("%s\n", __func__);
        return 0;
}

static int rtc_open (struct inode *pinode, struct file *fp)
{
        printk("%s\n", __func__);
        return 0;
}

static int rtc_release (struct inode *pinode, struct file *fp)
{
        printk("%s\n", __func__);
        return 0;
}

static const struct file_operations rtc_fops = {
        .owner = THIS_MODULE,
        .read = rtc_read,
        .write = rtc_write,
        .open = rtc_open,
        .release = rtc_release,
};

static dev_t dev;
static struct cdev *rtc_cdev;

static int __init rtc_init(void)
{
        // 构建cdev
        rtc_cdev = cdev_alloc();
        cdev_init(rtc_cdev, &rtc_fops);

        // 申请设备号
        dev = MKDEV(222, 0);
        register_chrdev_region(dev, 1, "rtc-demo");

        // 将cdev加入kobj树
        cdev_add(rtc_cdev, dev, 1);

        return 0;
}

static void __exit rtc_exit(void)
{
        // 将cdev从kobj树移除
        cdev_del(rtc_cdev);

        // 释放设备号
        unregister_chrdev_region(dev, 1);
}

module_init(rtc_init);
module_exit(rtc_exit);

MODULE_LICENSE("GPL");

动态申请设备号

主设备号高12位,次设备号低20位
设备号的转换

#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

        // 申请设备号
#if 0
        dev = MKDEV(222, 0);
        register_chrdev_region(dev, 1, "rtc-demo");
#else
        alloc_chrdev_region(&dev, 0, 1, "rtc-demo");
        printk("major : %d, minor : %d\n", MAJOR(dev), MINOR(dev));
#endif

自动创建设备节点

由于动态申请设备号所以需要自动创建设备节点

udev/mdev : 一个用户空间程序,通过扫描sysfs获得内核编号,用于创建设备节点。此外该程序还依赖 tmpfs。

为了udev获得驱动的信息,驱动必须将信息导出到sysfs,方法是 class_create , device_create

  /**
   * class_create - create a struct class structure
   * @owner: pointer to the module that is to "own" this struct class
   * @name: pointer to a string for the name of this class.
   *
   * This is used to create a struct class pointer that can then be used
   * in calls to device_create().
   *
   * Returns &struct class pointer on success, or ERR_PTR() on error.
   *
   * Note, the pointer created here is to be destroyed when finished by
   * making a call to class_destroy().
   */
  #define class_create(owner, name)       \
  ({                      \
      static struct lock_class_key __key; \
      __class_create(owner, name, &__key);    \
  })


  /**
   * device_create - creates a device and registers it with sysfs
   * @class: pointer to the struct class that this device should be registered to
   * @parent: pointer to the parent struct device of this new device, if any
   * @devt: the dev_t for the char device to be added
   * @drvdata: the data to be added to the device for callbacks
   * @fmt: string for the device's name
   *
   * This function can be used by char device classes.  A struct device
   * will be created in sysfs, registered to the specified class.
   *
   * A "dev" file will be created, showing the dev_t for the device, if
   * the dev_t is not 0,0.
   * If a pointer to a parent struct device is passed in, the newly created
   * struct device will be a child of that device in sysfs.
   * The pointer to the struct device will be returned from the call.
   * Any further sysfs files that might be required can be created using this
   * pointer.
   *
   * Returns &struct device pointer on success, or ERR_PTR() on error.
   *
   * Note: the struct class passed to this function must have previously
   * been created with a call to class_create().
   */
  struct device *device_create(struct class *class, struct device *parent,
                   dev_t devt, void *drvdata, const char *fmt, ...)
  {
      va_list vargs;
      struct device *dev;

      va_start(vargs, fmt);
      dev = device_create_groups_vargs(class, parent, devt, drvdata, NULL,
                        fmt, vargs);
      va_end(vargs);
      return dev;
  }

static const struct file_operations rtc_fops = {
        .owner = THIS_MODULE,
        .read = rtc_read,
        .write = rtc_write,
        .open = rtc_open,
        .release = rtc_release,
};

static dev_t dev;
static struct cdev *rtc_cdev;

static struct class *rtc_test_class;
static struct device *rtc_device;

static int __init rtc_init(void)
{
        // 构建cdev
        rtc_cdev = cdev_alloc();
        cdev_init(rtc_cdev, &rtc_fops);

        // 申请设备号
#if 0
        dev = MKDEV(222, 0);
        register_chrdev_region(dev, 1, "rtc-demo");
#else
        alloc_chrdev_region(&dev, 0, 1, "rtc-demo");
        printk("major : %d, minor : %d\n", MAJOR(dev), MINOR(dev));
#endif

        // 将cdev加入kobj树
        cdev_add(rtc_cdev, dev, 1);

        // 在/sys/class下创建rtc-class类
        rtc_test_class = class_create(THIS_MODULE, "rtc-class");
        if (IS_ERR(rtc_test_class)) {
                printk("class_create failed\n");
                return -1;
        }

        // 在/sys/class/rtc-class下创建rtc-demo%d设备
        rtc_device = device_create(rtc_test_class, NULL, dev, NULL, "rtc-demo%d", 0);
        if (IS_ERR(rtc_device)) {
                printk("device_create failed\n");
                return -1;
        }

        return 0;
}

static void __exit rtc_exit(void)
{
        // 将cdev从kobj树移除
        cdev_del(rtc_cdev);

        // 释放设备号
        unregister_chrdev_region(dev, 1);

        device_unregister(rtc_device);
        class_destroy(rtc_test_class);
}

module_init(rtc_init);
module_exit(rtc_exit);

加载模块后,发现/sys/class已经创建了rtc-class类

yangxr@vexpress:/root # ls /sys/class/rtc-class/
rtc-demo0
yangxr@vexpress:/root # ls /sys/class/rtc-class/rtc-demo0/
dev        power      subsystem  uevent
yangxr@vexpress:/root # cat /sys/class/rtc-class/rtc-demo0/dev
248:0
yangxr@vexpress:/root # cat /sys/class/rtc-class/rtc-demo0/uevent
MAJOR=248
MINOR=0
DEVNAME=rtc-demo0

运行mdev -s ,创建设备节点

yangxr@vexpress:/root # mdev -s
yangxr@vexpress:/root # ls /dev/rtc-demo0
/dev/rtc-demo0

填充fops

需要注意 用户空间和内核空间基于指针输入输出数据时,相关指向用户空间的指针要用 __user 描述。

填充read write

  inline static unsigned long rtc_tm_to_time(struct rtc_time_s *ptm)
  {
      return ptm->hour * 3600 + ptm->min * 60 + ptm->sec;
  }

  static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
  {
      struct rtc_time_s tm;
      int len = sizeof(tm);

      if ( !access_ok(buf, len))
          return -1;
      if (copy_from_user(&tm, buf, len) != 0) {
          printk("rtc_write failed\n");
          return -1;
      }
      regs->RTCLR = rtc_tm_to_time(&tm);

      return len;
  }

  static void rtc_time_s_trans(unsigned long n, struct rtc_time_s *p)
  {
      p->hour = (n % 86400) / 3600;
      p->min = (n % 3600) / 60;
      p->sec = (n % 60);
  }

  static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
  {
      unsigned long cur_time = regs->RTCDR;
      struct rtc_time_s tm;
      int len = sizeof(tm);

      rtc_time_s_trans(cur_time, &tm);
      if (copy_to_user(buf, &tm, len) != 0) {
          printk("rtc_read error\n");
          return -1;
      }

      return len;
  }

ioctl

  struct file_operations {
      ...
      long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
      long (*compat_ioctl) (struct file *, unsigned int, unsigned long);    // 需要兼容不同位宽时使用
  };

ioctl 对应两种回调,常用 unlocked_ioctl

传参都为

long (*unlocked_ioctl) (struct file *fp, unsigned int cmd, unsigned long arg);

其中cmd使用ioctl的宏设置

#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

比如定义设置RTC时间的命令,和读取RTC时间的命令

  #define RTC_CMD_MAGIC 'R'
  #define RTC_CMD_GET _IOR(RTC_CMD_MAGIC, 0, struct rtc_time_s *)
  #define RTC_CMD_SET _IOW(RTC_CMD_MAGIC, 1, struct rtc_time_s *)

ioctl实现

  static long rtc_ioctl (struct file *fp, unsigned int cmd, unsigned long arg)
  {
      struct rtc_time_s __user *buf = (struct rtc_time_s __user *)arg;
      unsigned long cur_time;
      struct rtc_time_s tm;

      switch (cmd) {
          case RTC_CMD_GET:
              cur_time = regs->RTCDR;
              rtc_time_s_trans(cur_time, &tm);
              if (copy_to_user(buf, &tm, sizeof(tm)) != 0) {
                  return -1;
              }
              break;

          case RTC_CMD_SET:
              if (copy_from_user(&tm, buf, sizeof(tm)) != 0) {
                  return -1;
              }
              cur_time = rtc_tm_to_time(&tm);
              regs->RTCLR = cur_time;
              break;

          default:
              return -1;
      }
      return 0;
  }

private_data

  struct file {
      ...
      /* needed for tty driver, and maybe others */
      void            *private_data;
  };

fp->private_data可用于实现驱动会话。
如在 open 操作时分配会话上下文,挂在 private_data上,其他操作调用时 fp->private_data 可以获得当前用户的会话信息。

提高驱动的安全性

为了避免用户非法输入导致内核挂掉,需要先检查用户数据。
常见的检查方法:
检查ioctl命令
_IOC_TYPE(cmd) 判断命令type是否合法
_IOC_DIR(cmd) 判断命令是读还是写
检查用户内存地址是否合法
access_ok(addr, sz) 判断用户传递的内存是否合法
返回值:1 成功,0 失败
有些函数内部自带检测:copy_from_user, copy_to_user, get_user, put_user
分支预测优化:likely, unlikely
比如

      if (copy_from_user(&tm, buf, len) != 0) {  // 由于这里出错可能性低
          printk("rtc_write failed\n");          // 所以这里不应该先缓存
          return -1;
      }
      regs->RTCLR = rtc_tm_to_time(&tm);         // 应该先缓存这里

使用 unlikely 告诉编译器为真的分支大概率不会执行,于是cache缓存时会跳过那部分指令

      if (unlikely(copy_from_user(&tm, buf, len) != 0)) {
          printk("rtc_write failed\n");
          return -1;
      }
      regs->RTCLR = rtc_tm_to_time(&tm);

制作库

由于直接基于文件系统导出的驱动接口不能直观描述接口功能,所以还需要做一个库,库直接操作驱动接口,用户使用库实现应用。
如 rtc的库可以这样声明

#ifndef __RTCLIB_H
#define __RTCLIB_H

#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct rtc_time {
        unsigned int year;
        unsigned int mon;
        unsigned int day;
        unsigned int hour;
        unsigned int min;
        unsigned int sec;
};

int rtc_open(const char *pathname);
int rtc_close(int fd);
int rtc_set_time(int fd, struct rtc_time *ptm);
int rtc_get_time(int fd, struct rtc_time *ptm);

#endif

示例

有三个led灯,创建4个设备 /dev/leds /dev/led1 /dev/led2 /dev/led3,提供开启和关闭操作

#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/module.h>

dev_t dev;
struct cdev *cdev;
struct class *led_class;

static void *gpio_va;
#define GPIO_OFT(x) ((x) - 0x56000000)
#define GPFCON (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
#define GPFDAT (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))

#define LED_NUM 3
#define DEVICE_NAME "led"

ssize_t led_write (struct file *fp, const char __user *user, size_t sz, loff_t *offset)
{
        unsigned int minor, val;

        /* 获得当前设备的此设备号 */
        minor = MINOR(fp->f_path.dentry->d_inode->i_rdev);
        if (copy_from_user(&val, user, sz) != 0) {
                printk(DEVICE_NAME "Failed to copy_from_user\n");
                return -1;
        }
        switch (minor) {
                case 0:  /* /dev/leds */
                        if (val == 0)
                                GPFDAT |= (1 << 4) | (1 << 5) | (1 << 6);
                        else
                                GPFDAT &= ~((1 << 4) | (1 << 5) | (1 << 6));
                        break;

                case 1:  /* /dev/led1 */
                        if (val == 0)
                                GPFDAT |= (1 << 4);
                        else
                                GPFDAT &= ~(1 << 4);

                        break;

                case 2: /* /dev/led2 */
                        if (val == 0)
                                GPFDAT |= (1 << 5);
                        else
                                GPFDAT &= ~(1 << 5);
                        break;

                case 3: /* /dev/led3 */
                        if (val == 0)
                                GPFDAT |= (1 << 6);
                        else
                                GPFDAT &= ~(1 << 6);
                        break;
        }

        return 0;
}

static int led_open (struct inode *inode, struct file *fp)
{
        /* 将GPIO设置为输出模式 */
        GPFCON &= ~((0x3 << 8) | (0x3 << 10) | (0x3 << 12));
        GPFCON |= (1 << 8) | (1 << 10) | (1 << 12);

        return 0;
}

static const struct file_operations led_ops = {
        .owner = THIS_MODULE,
        .open = led_open,
        .write = led_write,
};

static int __init led_init(void)
{
        unsigned int major, i;

        /* 把寄存器映射从物理地址到虚拟地址 */
        gpio_va = ioremap(0x56000000, 0x100000);
        if (gpio_va == NULL)
                return -EIO;

        /* 动态申请主设备号, 次设备号从0开始,一个三个设备 */
        if (alloc_chrdev_region(&dev, 0, LED_NUM + 1, "led") < 0) {
                printk(DEVICE_NAME "Failed to alloc dev number\n");
                return -1;
        }

        /* 构造cdev */
        if ((cdev = cdev_alloc()) == NULL) {
                printk(DEVICE_NAME "Failed to alloc cdev\n");
                return -1;
        }
        cdev->owner = THIS_MODULE;
        cdev->ops = &led_ops;

        /* 将cdev加入cdev_map,这样才能通过设备号找到cdev */
        if (cdev_add(cdev, dev, LED_NUM + 1) < 0) {
                printk(DEVICE_NAME "Failed to add cdev to system\n");
                return -1;
        }

        /* 创建class,方便mdev扫描/sys以添加设备节点 */
        led_class = class_create(THIS_MODULE, "led");
        if (led_class == NULL) {
                printk(DEVICE_NAME "Failed to create class\n");
                return -1;
        }
        /* 创建4个device,主设备号由系统分配,此设备号从0开始 */
        major = MAJOR(dev);
        device_create(led_class, NULL, MKDEV(major, 0), "leds");
        for (i = 1; i < LED_NUM + 1; i++) {
                if (device_create(led_class, NULL, MKDEV(major, i), "led%d", i) == NULL) {
                        printk(DEVICE_NAME "Failed to creat dev led%d", i);
                        return -1;
                }
        }

        printk(DEVICE_NAME "init\n");

        return 0;
}

static void __exit led_exit(void)
{
        unsigned int major, i;

        /* 释放4个device */
        major = MAJOR(cdev->dev);
        for (i = 0; i < LED_NUM + 1;  i++) {
                device_destroy(led_class, MKDEV(major, i));
        }

        /* 释放class */
        class_destroy(led_class);

        /* 将cdev从cdev_map中移除, 并释放cdev */
        cdev_del(cdev);

        /* 释放主设备号 */
        unregister_chrdev_region(dev, 0);

        /* 释放虚拟空间 */
        iounmap(gpio_va);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

原文作者:开心种树

原文地址:kernel--字符设备驱动 - 开心种树 - 博客园(版权归原文作者所有,侵权留言联系删除)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值