LINUX字符设备驱动学习

  • 《linux设备驱动程序》第三章重点讲解了字符设备的驱动程序编写,并对其中的细节做了详细介绍。下面对本章部分做一个详细的学习总结。

1.字符设备的定义

  • linux系统将设备分成三种基本类型:字符设备、块设备、网络接口。字符设备是个能够像字节流一样被访问的设备,字符设备驱动程序一般至少支持open、close、read、write系统调用。字符设备可以通过文件系统节点来访问,这种文件和普通文本文件之间的区别是,普通文件的访问支持随机访问,但是字符设备文件只支持顺序访问,类似于FIFO队列一样。

2.字符设备的应用场景

  • 字符设备一般用于比较简单的设备,比如字符终端(/dev/console)和串口(/dev/tty),I2C设备等等,这种是典型的字符设备,读取设备参数长度较小,设备返回的数据并不需要占用特别大的内存空间,并且对实时性要求不是特别高。控制命令也较为简单。在本章学习过程中,书中给出了一个Scull字符设备的完整实现过程。

3.字符设备的初始化

对设备的访问主要是通过文件系统内的设备名称来访问。这些设备名称被称为特殊文件、设备文件,或者简单称之为文件系统树的节点,他们通常位于“/dev”目录下。我们在“/dev”目录下可以看到如下,每一个设备都包含有一个主设备号和次设备号:

mike@mike-VirtualBox:/dev$ ls -l
total 0
crw-------  1 root    root     10, 235 2016-06-27 23:28 autofs
drwxr-xr-x  2 root    root         620 2016-06-27 23:28 block
drwxr-xr-x  2 root    root          80 2016-06-27 23:28 bsg
crw-------  1 root    root     10, 234 2016-06-27 23:28 btrfs-control
drwxr-xr-x  3 root    root          60 2016-06-27 23:28 bus
lrwxrwxrwx  1 root    root           3 2017-08-23 00:00 cdrom -> sr0
drwxr-xr-x  2 root    root        2840 2017-08-18 00:16 char
crw-------  1 root    root      5,   1 2016-06-27 23:27 console
lrwxrwxrwx  1 root    root          11 2016-06-27 23:28 core -> /proc/kcore
drwxr-xr-x  2 root    root          60 2016-06-27 23:28 cpu
crw-------  1 root    root     10,  58 2016-06-27 23:28 cpu_dma_latency
drwxr-xr-x  6 root    root         120 2017-08-23 00:00 disk
drwxr-xr-x  2 root    root          60 2016-06-27 23:29 dri
lrwxrwxrwx  1 root    root           3 2017-08-23 00:00 dvd -> sr0
  • 3.1生成设备注册号
    设备的注册码可以通过静态生成或者向系统动态申请。在内核中,dev_t类型用来保存设备编号–包括主设备号和次设备号。在内核2.6.0版本中,dev_t是一个32位的无符号整数,其中的12位用来表示主设备号,而其余20位用来表示次设备号。使用宏可以获取主设备号和次设备号:
MAJOR(dev_t dev);
MINOR(dev_t dev);
MKDEV(int major,int minor);//生成设备编号
静态生成注册号:
dev_t devno = MKDEV(mem_major, 0);

  /* 静态申请设备号*/
  if (mem_major)
    result = register_chrdev_region(devno, 2,"memdev");
动态申请设备注册号:
  result = alloc_chrdev_region(&devno, 0, 2,"memdev");
    mem_major = MAJOR(devno);
  • 3.2系统内核注册设备
    先初始化cdev结构,然后在内核中注册该设备。
/*初始化cdev结构*/
  cdev_init(&cdev,&mem_fops);
  cdev.owner = THIS_MODULE;
  cdev.ops =&mem_fops;//初始化OPS结构

  /* 注册字符设备 */
  cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

3.3为该设备申请内存

 /* 为设备描述结构分配内存*/
  mem_devp = kmalloc(MEMDEV_NR_DEVS* sizeof(struct mem_dev), GFP_KERNEL);
  if (!mem_devp)/*申请失败*/
  {
    result = - ENOMEM;
    goto fail_malloc;
  }
  memset(mem_devp, 0,sizeof(struct mem_dev));

  /*为设备分配内存*/
  for (i=0; i< MEMDEV_NR_DEVS; i++)
  {
        mem_devp[i].size= MEMDEV_SIZE;
        mem_devp[i].data= kmalloc(MEMDEV_SIZE, GFP_KERNEL);
        memset(mem_devp[i].data, 0, MEMDEV_SIZE);
  }

4.字符设备的读写

  • 设备的读写操作需要通过file_operation结构来注册,在设备初始化时,通过该结构体将设备的用户读写操作控制与自定义的操作函数关联起来。file_operations结构的指针称为fops。这个结构中的每一个字段都必须指向驱动程序中实现特定操作的函数。在本例中,我们定义fops结构如下:
/*文件操作结构体*/
static conststruct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .ioctl = mem_ioctl,
  .release = mem_release,
};
  • 在linux内核原代码中,我们可以查看fops结构的详细定义如下,分别定义了总共种26操作,关于每种操作的详细定义及用法可以参考相关的说明文档和书籍,但我们常用的几种操作如read、write、release、ioctl等操作需要详细了解。fops结构体其定义如下:
/*fops 定义*/
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};
struct inode {
    struct hlist_node   i_hash;
    struct list_head    i_list;
    struct list_head    i_sb_list;
    struct list_head    i_dentry;
    unsigned long       i_ino;
    atomic_t        i_count;
    unsigned int        i_nlink;
    uid_t           i_uid;
    gid_t           i_gid;
    dev_t           i_rdev;
    u64         i_version;
    loff_t          i_size;
#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t      i_size_seqcount;
#endif
    struct timespec     i_atime;
    struct timespec     i_mtime;
    struct timespec     i_ctime;
    unsigned int        i_blkbits;
    blkcnt_t        i_blocks;
    unsigned short          i_bytes;
    umode_t         i_mode;
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    struct mutex        i_mutex;
    struct rw_semaphore i_alloc_sem;
    const struct inode_operations   *i_op;
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct super_block  *i_sb;
    struct file_lock    *i_flock;
    struct address_space    *i_mapping;
    struct address_space    i_data;
#ifdef CONFIG_QUOTA
    struct dquot        *i_dquot[MAXQUOTAS];
#endif
    struct list_head    i_devices;
    union {
        struct pipe_inode_info  *i_pipe;
        struct block_device *i_bdev;
        struct cdev     *i_cdev;
    };
    int         i_cindex;

    __u32           i_generation;

#ifdef CONFIG_DNOTIFY
    unsigned long       i_dnotify_mask; /* Directory notify events */
    struct dnotify_struct   *i_dnotify; /* for directory notifications */
#endif

#ifdef CONFIG_INOTIFY
    struct list_head    inotify_watches; /* watches on this inode */
    struct mutex        inotify_mutex;  /* protects the watches list */
#endif

    unsigned long       i_state;
    unsigned long       dirtied_when;   /* jiffies of first dirtying */

    unsigned int        i_flags;

    atomic_t        i_writecount;
#ifdef CONFIG_SECURITY
    void            *i_security;
#endif
    void            *i_private; /* fs or device private pointer */
};
  • 在本例中自定义的open,write,close等操作定义如下:
/*文件打开函数*/
int mem_open(struct inode*inode, struct file *filp)
{
    struct mem_dev *dev;

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num>= MEMDEV_NR_DEVS)
            return -ENODEV;
    dev = &mem_devp[num];

    /*将设备描述结构指针赋值给文件私有数据指针*/
    filp->private_data= dev;

    return 0;
}
/*读函数*/
static ssize_t mem_read(structfile *filp,char __user *buf,size_t size, loff_t*ppos)
{
  unsigned long p= *ppos;
  unsigned intcount = size;
  int ret = 0;
  struct mem_dev *dev= filp->private_data;/*获得设备结构体指针*/

  /*判断读位置是否有效*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count> MEMDEV_SIZE - p)
    count = MEMDEV_SIZE- p;

  /*读数据到用户空间*/
  if (copy_to_user(buf,(void*)(dev->data+ p),count))
  {
    ret = - EFAULT;
  }
  else
  {
    *ppos +=count;
    ret = count;

    printk(KERN_INFO "read %d bytes(s) from %d/n", count, p);
  }

  return ret;
}
/*写函数*/
static ssize_t mem_write(structfile *filp,const char __user*buf, size_t size, loff_t *ppos)
{
  unsigned long p= *ppos;
  unsigned intcount = size;
  int ret = 0;
  struct mem_dev *dev= filp->private_data;/*获得设备结构体指针*/

  /*分析和获取有效的写长度*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count> MEMDEV_SIZE - p)
    count = MEMDEV_SIZE- p;

  /*从用户空间写入数据*/
  if (copy_from_user(dev->data+ p, buf,count))
    ret = - EFAULT;
  else
  {
    *ppos +=count;
    ret = count;

    printk(KERN_INFO "written %d bytes(s) from %d/n", count, p);
  }

  return ret;
}

5.字符设备的控制

  • 本设备自定义控制命令字如下:
 typedef union
 {
     MEM_GET_INFO/*获取设备信息*/
     MEM_SET_INFO/*设置设备信息*/
     MEM_GET_NUM/*获取设备数量*/
     MEM_GET_MEMORY_USAGE/*获取当前已经使用的内存*/
};
/*设备IOCTL命令*/
static ssize_t mem_ioctl(struct inode *inode, struct file *file, unsigned int cmd unsigned long arg){
    struct mem_dev *dev;
    void  *buffer = NULL;
    void  *argp = (void *)arg;//设备的控制命令

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num>= MEMDEV_NR_DEVS)
            return -ENODEV;

    switch(cmd){
        case MEM_GET_INFO:
          copy_to_user((char *)argp,mem_info[num],strlen(mem_info[num]));
          break;
        case MEM_SET_INFO:
          memset(mem_info[num],0,sizeof(char)*MEMDEV_INFO_SIZE);
          copy_from_user(mem_info[num],(char *)argp,strlen((char *)argp));
          break;
        case MEM_GET_NUM:
          (int *)argp = 2;
          break;
        case MEM_GET_MEMORY_USAGE:
          (int *)argp = MEMDEV_SIZE;
          break;
        default:
          break;
    }

    return 0;

}

6.字符设备的销毁

  • 设备销毁时需要注销设备,并释放内存,同时释放设备。注意在释放内存时一定要将所有动态申请的内存全部释放掉,否则出现内存泄漏。
/*模块卸载函数*/
static void memdev_exit(void)
{
  cdev_del(&cdev);/*注销设备*/
   /*为设备分配内存*/
  for (i=0; i< MEMDEV_NR_DEVS; i++)
  {   
      kfree(mem_devp[i].data);
  }
  kfree(mem_devp);/*释放设备结构体内存*/

  unregister_chrdev_region(MKDEV(mem_major, 0), 2);/*释放设备号*/
}

7.代码示例

  • 以下为网上示例代码,并且通过测试验证,完全能够正常运行。
/******************************************************************************
*Name: memdev.c
*Desc: 字符设备驱动程序的框架结构,该字符设备并不是一个真实的物理设备,
* 而是使用内存来模拟一个字符设备
*Parameter: 
*Return:
*Author: yoyoba(stuyou@126.com)
*Date: 2010-9-26
*Modify: 2010-9-26
********************************************************************************/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

#include "memdev.h"

static mem_major = MEMDEV_MAJOR;

module_param(mem_major,int, S_IRUGO);

struct mem_dev *mem_devp;/*设备结构体指针*/

char ** mem_info = NULL;//设备信息描述

struct cdev cdev;



/*文件打开函数*/
int mem_open(struct inode*inode, struct file *filp)
{
    struct mem_dev *dev;

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num>= MEMDEV_NR_DEVS)
            return -ENODEV;
    dev = &mem_devp[num];

    /*将设备描述结构指针赋值给文件私有数据指针*/
    filp->private_data= dev;

    return 0;
}

/*文件释放函数*/
int mem_release(struct inode*inode, struct file *filp)
{
  return 0;
}

/*读函数*/
static ssize_t mem_read(struct file *filp,char __user *buf,size_t size, loff_t*ppos)
{
  unsigned long p= *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev= filp->private_data;/*获得设备结构体指针*/

  /*判断读位置是否有效*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count> MEMDEV_SIZE - p)
    count = MEMDEV_SIZE- p;

  /*读数据到用户空间*/
  if (copy_to_user(buf,(void*)(dev->data+ p),count))
  {
    ret = - EFAULT;
  }
  else
  {
    *ppos +=count;
    ret = count;

    printk(KERN_INFO "read %d bytes(s) from %d/n", count, p);
  }

  return ret;
}

/*写函数*/
static ssize_t mem_write(struct file *filp,const char __user*buf, size_t size, loff_t *ppos)
{
  unsigned long p= *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev= filp->private_data;/*获得设备结构体指针*/

  /*分析和获取有效的写长度*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count> MEMDEV_SIZE - p)
    count = MEMDEV_SIZE- p;

  /*从用户空间写入数据*/
  if (copy_from_user(dev->data+ p, buf,count))
    ret = - EFAULT;
  else
  {
    *ppos +=count;
    ret = count;

    printk(KERN_INFO "written %d bytes(s) from %d/n", count, p);
  }

  return ret;
}

/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset,int whence)
{ 
    loff_t newpos;

    switch(whence){
      case 0:/* SEEK_SET */
        newpos = offset;
        break;

      case 1:/* SEEK_CUR */
        newpos = filp->f_pos+ offset;
        break;

      case 2:/* SEEK_END */
        newpos = MEMDEV_SIZE -1 - offset;
        break;

      default:/* can't happen */
        return -EINVAL;
    }
    if ((newpos<0)|| (newpos>MEMDEV_SIZE))
     return -EINVAL;

    filp->f_pos= newpos;
    return newpos;

}

/*设备IOCTL命令*/
static ssize_t mem_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg){
    struct mem_dev *dev;
    void  *buffer = NULL;
    void  *argp = (void *)arg;//设备的控制命令

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num>= MEMDEV_NR_DEVS)
            return -ENODEV;

    switch(cmd){
        case MEM_GET_INFO:
          copy_to_user((char *)argp,mem_info[num],strlen(mem_info[num]));
          break;
        case MEM_SET_INFO:
          memset(mem_info[num],0,sizeof(char)*MEMDEV_INFO_SIZE);
          copy_from_user(mem_info[num],(char *)argp,strlen((char *)argp));
          break;
        case MEM_GET_NUM:
          (*(int *)argp) = 2;
          break;
        case MEM_GET_MEMORY_USAGE:
          (*(int *)argp) = MEMDEV_SIZE;
          break;
        default:
          break;
    }

    return 0;

}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .ioctl = mem_ioctl,
  .release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
  int result;
  int i;

  dev_t devno = MKDEV(mem_major, 0);

  /* 静态申请设备号*/
  if (mem_major)
    result = register_chrdev_region(devno, 2,"memdev");
  else /* 动态分配设备号 */
  {
    result = alloc_chrdev_region(&devno, 0, 2,"memdev");
    mem_major = MAJOR(devno);
  } 

  if (result< 0)
    return result;


  /*初始化cdev结构*/
  cdev_init(&cdev,&mem_fops);
  cdev.owner = THIS_MODULE;
  cdev.ops =&mem_fops;

  /* 注册字符设备 */
  cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

  /*为设备描述信息分配内存*/
  mem_info = kmalloc(MEMDEV_NR_DEVS*sizeof(char *),GFP_KERNEL);
  if(!mem_info){
    result = - ENOMEM;
    goto fail_malloc1;
  }
  for (i=0; i< MEMDEV_NR_DEVS; i++){
    mem_info[i] = kmalloc(MEMDEV_INFO_SIZE*sizeof(char),GFP_KERNEL);
    memset(mem_info[i],0,MEMDEV_INFO_SIZE*sizeof(char));
    if(!mem_info[i]){
      goto fail_malloc1;
    }
    memcpy(mem_info[i],"this is my memory device!",sizeof("this is my memory device!"));
  }


  /* 为设备描述结构分配内存*/
  mem_devp = kmalloc(MEMDEV_NR_DEVS* sizeof(struct mem_dev), GFP_KERNEL);
  if (!mem_devp)/*申请失败*/
  {
    result = - ENOMEM;
    goto fail_malloc2;
  }
  memset(mem_devp, 0,sizeof(struct mem_dev));

  /*为设备分配内存*/
  for (i=0; i< MEMDEV_NR_DEVS; i++)
  {
        mem_devp[i].size= MEMDEV_SIZE;
        mem_devp[i].data= kmalloc(MEMDEV_SIZE, GFP_KERNEL);
        memset(mem_devp[i].data, 0, MEMDEV_SIZE);
  }

  return 0;

  fail_malloc2: 
  for (i=0; i< MEMDEV_NR_DEVS; i++){
    kfree(mem_info[i]);
  }
  kfree(mem_info);//需释放内存
  fail_malloc1:
  unregister_chrdev_region(devno, 1);

  return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
  int i;
  cdev_del(&cdev);/*注销设备*/
   /*为设备分配内存*/
  for (i=0; i< MEMDEV_NR_DEVS; i++)
  {   
      kfree(mem_devp[i].data);
  }
  kfree(mem_devp);/*释放设备结构体内存*/

  unregister_chrdev_region(MKDEV(mem_major, 0), 2);/*释放设备号*/
}

MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);
/************************
*memdev.h
************************/
#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 260/*预设的mem的主设备号*/
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2/*设备数*/
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif

#ifndef MEMDEV_INFO_SIZE
#define MEMDEV_INFO_SIZE 128/*描述信息长度*/
#endif 

/*mem设备描述结构体*/
struct mem_dev 
{ 
  char *data;
  unsigned long size;
};

typedef enum
 {
     MEM_GET_INFO = 1,/*获取设备信息*/
     MEM_SET_INFO,/*设置设备信息*/
     MEM_GET_NUM,/*获取设备数量*/
     MEM_GET_MEMORY_USAGE,/*获取当前已经使用的内存*/
}_MEM_IOCTL_E_;

#endif /* _MEMDEV_H_ */
/******************************************************************************
*Name: memdevapp.c
*Desc: memdev字符设备驱动程序的测试程序。先往memedev设备写入内容,然后再
* 从该设备中把内容读出来。
*Parameter: 
*Return:
*Author: yoyoba(stuyou@126.com)
*Date: 2010-9-26
*Modify: 2010-9-26
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/fcntl.h>
#include <sys/ioctl.h>
#include  "memdev.h"
int main()
{
 int fd;
 int rt_val;
 char buf[]="this is a example for character devices driver by yoyoba!";//写入memdev设备的内容

 char buf_read[4096];//memdev设备的内容读入到该buf中


 if((fd=open("/dev/memdev",O_RDWR))==-1)//打开memdev设备
  printf("open memdev WRONG!\n\r");
 else
  printf("open memdev SUCCESS!\n\r");

 printf("buf is %s\n\r",buf);

 write(fd,buf,sizeof(buf));//把buf中的内容写入memdev设备


 lseek(fd,0,SEEK_SET);//把文件指针重新定位到文件开始的位置


 read(fd,buf_read,sizeof(buf));//把memdev设备中的内容读入到buf_read中

 printf("buf_read is %s\n\r",buf_read);
 rt_val = ioctl(fd,MEM_GET_INFO,buf_read);
 if(rt_val!=-1){
     printf("buf_read is %s\n\r",buf_read);
 }



 return 0;
}
ifneq ($(KERNELRELEASE),)

obj-m:=memdev.o

else

KERNELDIR:=/lib/modules/$(shell uname -r)/build

PWD:=$(shell pwd)

default:

    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:

    rm -rf *.o *.mod.c *.mod.o *.ko

endif

insmod memdev.ko
mknod memdev c 260 0
crw-r-----  1 root    kmem      1,   1 2017-08-14 15:40 mem
crw-r--r--  1 root    root    260,   0 2017-08-24 18:25 memdev
root@mike-VirtualBox:/mnt/share/linux-kernel/part-3# ./memapp
open memdev SUCCESS!
buf is this is a example for character devices driver by yoyoba!
buf_read is this is a example for character devices driver by yoyoba!
buf_read is this is my memory device!racter devices driver by yoyoba!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值