嵌入式开发记录-补充02:IO模型

1、阻塞:参考:https://www.sohu.com/a/258717832_781584
  当条件不满足的时候,应用进程睡眠;

struct __wait_queue {
  unsigned int flags;
  void *private;
  wait_queue_func_t func;
  struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;

  1、初始化等待队列头,每个等待队列都有一个头;

init_waitqueue_head((wait_queue_head_t *q)

  2、初始化等待队列项,对应每个task.
    在read()函数中初始化等待队列项,在条件不满足的时候需要应用层睡眠,这里是应用层调用,因此current指到应用层;

void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p ) // current

  3、添加等待队列项到等待队列头的链表上;

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

  4、设置当前进程的状态

set_current_state(state_value) // 设置应用层的状态
    #define TASK_INTERRUPTIBLE 1
    #define TASK_UNINTERRUPTIBLE 2

   5、进程调度

void schedule(void) // 进程调度:进入睡眠状态,如果条件满足并唤醒,重新返回到这里运行.
//-------条件满足唤醒---------
wake_up(wait_queue_head_t *q)

wake_up_interruptible(wait_queue_head_t * x)

  6、从等待队列上移除等待队列项

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

  7、设置应用层状态--运行态

set_current_state(state_value)

  阻塞IO实现方式一:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/err.h>
#include <asm/uaccess.h>   // 适用于x86平台。
//#include <asm-generic/uaccess.h>  // 使用ARM平台。
#include <linux/wait.h>
#include <linux/sched.h>

#define COUNT 3
#define DEV_NAME "devDemo"

static dev_t  devNum=0;
static     struct cdev* mydev=NULL;
static struct class * dev_cls=NULL;
static struct device* devp=NULL;

static char kbuf[128]={'\0'};
static int kbufcount=0;
static int ma=240;

static wait_queue_head_t     wqh;
static wait_queue_t         wq;


static int myopen (struct inode *node, struct file *filp)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);

    return 0;
}
static ssize_t myread (struct file *filp, char __user *ubuf, size_t size, loff_t *off)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);

    if(kbufcount == 0){
        init_waitqueue_entry(&wq, current);      // 2.2 初始化等待队列项。
        add_wait_queue(&wqh, &wq);                // 2.3 等待队列项添加到等待队列头后面。
        set_current_state(TASK_INTERRUPTIBLE);    // 2.4 设置当前进程状态
        schedule();    // 上层应用进程将阻塞在这里    // 2.5 进程调度---进入睡眠,条件满足并返回
        set_current_state(TASK_RUNNING);        // 2.7 设置当前进程状态
        remove_wait_queue(&wqh, &wq);            // 2.8 将等待队列项移除
    }
    
    if(size > kbufcount)
        size = kbufcount;
    if(copy_to_user(ubuf, kbuf,13)){
        printk(KERN_INFO "copy to user failed\n");
        return -EAGAIN;
    }
    kbufcount =0;
    return 0;
}
static ssize_t mywrite (struct file *filp, const char __user * ubuf, size_t size, loff_t *off)
{
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);
    
    if(size > 128){
        size = 128;
    }
    if(copy_from_user(kbuf, ubuf, size)){
        printk(KERN_ERR "copy from user failed\n");
        return -EAGAIN;   // 拷贝失败应用层程序再尝试一次;
    }
    kbufcount = size;
    wake_up(&wqh);  // 2.6 唤醒队列头后面所有进程;
    
    printk(KERN_INFO "write buf:%s\n", kbuf);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return kbufcount;
}

static int myrelaese (struct inode *nod, struct file *file)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return 0;
}


static struct file_operations  fileops={
    .owner     = THIS_MODULE,
    .open     = myopen,
    .release    = myrelaese,
    .read    = myread,
    .write    = mywrite,
    
};

static int __init mdev_init(void)
{
    int ret=0,i;

    printk("Hello cdev\n");
    //ret = alloc_chrdev_region(&devNum, 0, 1, "dev_demo");
    devNum = MKDEV(ma,0);
    printk("dev major:%d.\n", MAJOR(devNum));
    printk("dev manor:%d.\n", MINOR(devNum));
    
    ret = register_chrdev_region(devNum, COUNT, DEV_NAME);
    if(ret != 0){
        printk("chr dev region err\n");
        goto err0;
    }
    // 1. 为cdev分配空间.
    mydev = cdev_alloc();
    if(mydev == NULL){
        printk(KERN_ERR "alloc cdev error\n");
        goto err1;
    }
    // 2. cdev初始化.
    cdev_init(mydev, &fileops);
    // 3. 添加cdev
    ret = cdev_add(mydev, devNum, COUNT);
    if(ret < 0 ){
        printk(KERN_ERR "dev add error\n");
        goto err1;
    }
    // 4. 在sys/class下创建目录
    dev_cls = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(dev_cls)){
        printk(KERN_ERR "class create failed:%ld\n", PTR_ERR(dev_cls));
        ret = PTR_ERR(dev_cls);
        goto err2;
    }
    // 5、创建设备结点,在dev目录下,创建名称为DEV_NAME+i的设备结点;
    for(i=0;i<3;i++){
        devp = device_create(dev_cls, NULL, MKDEV(ma,i), NULL, "%s%d", DEV_NAME,i);
        if(IS_ERR(devp)){
            printk(KERN_ERR "device create failed: %ld\n", PTR_ERR(devp));
            ret = PTR_ERR(devp);
            goto err3;
        }
    }
    //2.1 初始化等待队列头。
    init_waitqueue_head(&wqh);  // 初始化等待队列头。
    
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return 0;
    
err3:
    for(--i;i>=0;i--){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
err2:
    cdev_del(mydev);
    
err1:
    unregister_chrdev_region(devNum, COUNT);

err0:
    return 0;
}
static void __exit mdev_exit(void)
{
    int i=0;
    printk("Good bye\n");
    for(i=0;i<3;i++){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
    cdev_del(mydev);
    unregister_chrdev_region(devNum, COUNT);
}

module_init(mdev_init);
module_exit(mdev_exit);
MODULE_LICENSE("GPL");
View Code

  阻塞IO实现方式二:

    wait_event(wait_queue_head_t *wq, condition)
    wait_event_interruptible(wait_queue_head_t *wq, condition)     // 可中断的
    wake_up(wait_queue_head_t *q) 
    wake_up_interruptible(wait_queue_head_t * x)

2、非阻塞:
  当条件不满足的时候,返回错误给用进程;

  应用层的open操作默认为阻塞操作,那么如果需要非阻塞需要应用层传递非阻塞标志;flags中的O_NONBLOCK

  在驱动中:

if(filp->f_flags & O_NONBLOCK){
    return -EAGAIN;
}

3、IO多路复用:

  操作多个文件,对应多个文件描述符;每个文件的读写操作是否阻塞需要判断,如果有其中的几个文件读写操作不阻塞了,就是可以操作了,通过返回值返回这样的信息;

  其中对应的驱动的操作接口为:应用层调用select、poll、epoll都会调到下面这个接口;

unsigned int (*poll) (struct file *, struct poll_table_struct *);

  驱动中需要实现的,主要实现里面那个参数

static unsigned int mypoll (struct file *filp, struct poll_table_struct *pt)
{
    int mask =0;
    if(kbufcount){
        mask |= POLLIN;
    }
    // static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    poll_wait(filp, &wqh,pt);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return mask;
}
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/err.h>
#include <asm/uaccess.h>   // 适用于x86平台。
//#include <asm-generic/uaccess.h>  // 使用ARM平台。
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>


#define COUNT 3
#define DEV_NAME "devDemo"

static dev_t  devNum=0;
static     struct cdev* mydev=NULL;
static struct class * dev_cls=NULL;
static struct device* devp=NULL;

static char kbuf[128]={'\0'};
static int kbufcount=0;
static int ma=240;

static wait_queue_head_t     wqh;
static wait_queue_t         wq;


static int myopen (struct inode *node, struct file *filp)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);

    return 0;
}
static ssize_t myread (struct file *filp, char __user *ubuf, size_t size, loff_t *off)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);

    if(kbufcount == 0){
        if(filp->f_flags & O_NONBLOCK){
            return -EAGAIN;
        }
        init_waitqueue_entry(&wq, current);      // 2.2 初始化等待队列项。
        add_wait_queue(&wqh, &wq);                // 2.3 等待队列项添加到等待队列头后面。
        set_current_state(TASK_INTERRUPTIBLE);    // 2.4 设置当前进程状态
        schedule();    // 上层应用进程将阻塞在这里    // 2.5 进程调度---进入睡眠,条件满足并返回
        set_current_state(TASK_RUNNING);        // 2.7 设置当前进程状态
        remove_wait_queue(&wqh, &wq);            // 2.8 将等待队列项移除
    }

    if(size > kbufcount)
        size = kbufcount;
    if(copy_to_user(ubuf, kbuf,13)){
        printk(KERN_INFO "copy to user failed\n");
        return -EAGAIN;
    }
    kbufcount =0;
    return 0;
}
static ssize_t mywrite (struct file *filp, const char __user * ubuf, size_t size, loff_t *off)
{
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);
    
    if(size > 128){
        size = 128;
    }
    if(copy_from_user(kbuf, ubuf, size)){
        printk(KERN_ERR "copy from user failed\n");
        return -EAGAIN;   // 拷贝失败应用层程序再尝试一次;
    }
    kbufcount = size;

    wake_up(&wqh);  // 2.6 唤醒队列头后面所有进程;

    printk(KERN_INFO "write buf:%s\n", kbuf);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return kbufcount;
}

static int myrelaese (struct inode *nod, struct file *file)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return 0;
}

static unsigned int mypoll (struct file *filp, struct poll_table_struct *pt)
{
    int mask =0;
    if(kbufcount){
        mask |= POLLIN;
    }
    // static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    poll_wait(filp, &wqh,pt);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return mask;
}


static struct file_operations  fileops={
    .owner     = THIS_MODULE,
    .open     = myopen,
    .release    = myrelaese,
    .read    = myread,
    .write    = mywrite,
    .poll    = mypoll,
    
};

static int __init mdev_init(void)
{
    int ret=0,i;

    printk("Hello cdev\n");
    //ret = alloc_chrdev_region(&devNum, 0, 1, "dev_demo");
    devNum = MKDEV(ma,0);
    printk("dev major:%d.\n", MAJOR(devNum));
    printk("dev manor:%d.\n", MINOR(devNum));
    
    ret = register_chrdev_region(devNum, COUNT, DEV_NAME);
    if(ret != 0){
        printk("chr dev region err\n");
        goto err0;
    }
    // 1. 为cdev分配空间.
    mydev = cdev_alloc();
    if(mydev == NULL){
        printk(KERN_ERR "alloc cdev error\n");
        goto err1;
    }
    // 2. cdev初始化.
    cdev_init(mydev, &fileops);
    // 3. 添加cdev
    ret = cdev_add(mydev, devNum, COUNT);
    if(ret < 0 ){
        printk(KERN_ERR "dev add error\n");
        goto err1;
    }
    // 4. 在sys/class下创建目录
    dev_cls = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(dev_cls)){
        printk(KERN_ERR "class create failed:%ld\n", PTR_ERR(dev_cls));
        ret = PTR_ERR(dev_cls);
        goto err2;
    }
    // 5、创建设备结点,在dev目录下,创建名称为DEV_NAME+i的设备结点;
    for(i=0;i<3;i++){
        devp = device_create(dev_cls, NULL, MKDEV(ma,i), NULL, "%s%d", DEV_NAME,i);
        if(IS_ERR(devp)){
            printk(KERN_ERR "device create failed: %ld\n", PTR_ERR(devp));
            ret = PTR_ERR(devp);
            goto err3;
        }
    }
    //2.1 初始化等待队列头。
    init_waitqueue_head(&wqh);  // 初始化等待队列头。
    
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return 0;
    
err3:
    for(--i;i>=0;i--){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
err2:
    cdev_del(mydev);
    
err1:
    unregister_chrdev_region(devNum, COUNT);

err0:
    return 0;
}
static void __exit mdev_exit(void)
{
    int i=0;
    printk("Good bye\n");
    for(i=0;i<3;i++){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
    cdev_del(mydev);
    unregister_chrdev_region(devNum, COUNT);
}

module_init(mdev_init);
module_exit(mdev_exit);
MODULE_LICENSE("GPL");
View Code

  测试应用实例:

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

char rBuf[64];

int main(int argc, char* argv[])
{
    int fd =0,ret=0;
    fd_set rfds;
    printf("hello world\n");
    struct  UData dat={100,"liunx"};
    fd = open("/dev/devDemo1",O_RDWR);
    if(fd < 0){
        printf("open device failed.\n");
        perror("open");
        return -1;
    }

    while(1){
        FD_ZERO(&rfds);
        FD_SET(fd, &rfds);
        ret = select(fd+1,&rfds,  NULL, NULL, NULL);
        if(ret < 0){
            perror("select\n");
        }
        if(FD_ISSET(fd, &rfds)){
            read(fd,rBuf, 64);
            printf("select read:%s\n", rBuf);
        }
    }
    
    close(fd);
    return 0;
}
View Code

 

4、异步通知:

  基于通知机制---->信号的方式signal

  内核(驱动)--通知-->应用层进程
  内核发送SIGIO信号,那么应用程序异步接收信号,使用异步接收信号
  开启异步接收

fcntl(fd, F_SETFL, fcntl(fd,F_GETFL)|O_ASYNC);   

  设置异步接收拥有者:

fcntl(fd, F_SETOWN, getpid())

  内核实现异步通知:
    fcntl系统调用:do_fcntl
  do_fcntl(int fd, unsigned int cmd, unsigned long arg, struct file * filp)
    调用:int (*fasync) (int, struct file *, int);
  在该接口调用:fasync_helper

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

  初始化异步通知结构体,并插入异步通知队列中;如果条件满足了,发送信号到应用程序,应用程序接收信号,进行读操作;

static unsigned int mypoll (struct file *filp, struct poll_table_struct *pt)
{
    int mask =0;
    if(kbufcount){
        mask |= POLLIN;
    }
    // static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    poll_wait(filp, &wqh,pt);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return mask;
}

  内核给应用程序发送信号:

// void kill_fasync(struct fasync_struct **fp, int sig, int band)
kill_fasync(&fapp, SIGIO, POLL_IN);  // 发送信号
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/err.h>
#include <asm/uaccess.h>   // 适用于x86平台。
//#include <asm-generic/uaccess.h>  // 使用ARM平台。
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <asm/signal.h>


#define COUNT 3
#define DEV_NAME "devDemo"

static dev_t  devNum=0;
static     struct cdev* mydev=NULL;
static struct class * dev_cls=NULL;
static struct device* devp=NULL;

static char kbuf[128]={'\0'};
static int kbufcount=0;
static int ma=240;

static wait_queue_head_t     wqh;
static wait_queue_t         wq;
static struct fasync_struct * fapp=NULL;

static int myopen (struct inode *node, struct file *filp)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);

    return 0;
}
static ssize_t myread (struct file *filp, char __user *ubuf, size_t size, loff_t *off)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);

    if(kbufcount == 0){
        if(filp->f_flags & O_NONBLOCK){
            return -EAGAIN;
        }
        init_waitqueue_entry(&wq, current);      // 2.2 初始化等待队列项。
        add_wait_queue(&wqh, &wq);                // 2.3 等待队列项添加到等待队列头后面。
        set_current_state(TASK_INTERRUPTIBLE);    // 2.4 设置当前进程状态
        schedule();    // 上层应用进程将阻塞在这里    // 2.5 进程调度---进入睡眠,条件满足并返回
        set_current_state(TASK_RUNNING);        // 2.7 设置当前进程状态
        remove_wait_queue(&wqh, &wq);            // 2.8 将等待队列项移除
    }

    if(size > kbufcount)
        size = kbufcount;
    if(copy_to_user(ubuf, kbuf,13)){
        printk(KERN_INFO "copy to user failed\n");
        return -EAGAIN;
    }
    kbufcount =0;
    return 0;
}
static ssize_t mywrite (struct file *filp, const char __user * ubuf, size_t size, loff_t *off)
{
    //printk(KERN_INFO "buf addr:0x%x, szie:%d\n", ubuf, size);
    
    if(size > 128){
        size = 128;
    }
    if(copy_from_user(kbuf, ubuf, size)){
        printk(KERN_ERR "copy from user failed\n");
        return -EAGAIN;   // 拷贝失败应用层程序再尝试一次;
    }
    kbufcount = size;

    //wake_up(&wqh);  // 2.6 唤醒队列头后面所有进程;

    kill_fasync(&fapp, SIGIO, POLL_IN);  // 发送信号

    printk(KERN_INFO "write buf:%s\n", kbuf);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return kbufcount;
}

static int myrelaese (struct inode *nod, struct file *file)
{
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return 0;
}

static unsigned int mypoll (struct file *filp, struct poll_table_struct *pt)
{
    int mask =0;
    if(kbufcount){
        mask |= POLLIN;
    }
    // static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    poll_wait(filp, &wqh,pt);
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return mask;
}

// 异步IO
/*
int (*fasync) (int, struct file *, int);
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

内核中发信号:                                                // POLL_IN/ POLL_OUT
    kill_fasync(struct fasync_struct ** fp, int sig, int band)

*/
    
static int myfasync(int fd, struct file * filp, int isAsync)
{
    // 1、
    
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    return fasync_helper(fd, filp, isAsync, &fapp);;
}


static struct file_operations  fileops={
    .owner     = THIS_MODULE,
    .open     = myopen,
    .release    = myrelaese,
    .read    = myread,
    .write    = mywrite,
    .poll    = mypoll,
    .fasync    = myfasync,
    
};

static int __init mdev_init(void)
{
    int ret=0,i;

    printk("Hello cdev\n");
    //ret = alloc_chrdev_region(&devNum, 0, 1, "dev_demo");
    devNum = MKDEV(ma,0);
    printk("dev major:%d.\n", MAJOR(devNum));
    printk("dev manor:%d.\n", MINOR(devNum));
    
    ret = register_chrdev_region(devNum, COUNT, DEV_NAME);
    if(ret != 0){
        printk("chr dev region err\n");
        goto err0;
    }
    // 1. 为cdev分配空间.
    mydev = cdev_alloc();
    if(mydev == NULL){
        printk(KERN_ERR "alloc cdev error\n");
        goto err1;
    }
    // 2. cdev初始化.
    cdev_init(mydev, &fileops);
    // 3. 添加cdev
    ret = cdev_add(mydev, devNum, COUNT);
    if(ret < 0 ){
        printk(KERN_ERR "dev add error\n");
        goto err1;
    }
    // 4. 在sys/class下创建目录
    dev_cls = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(dev_cls)){
        printk(KERN_ERR "class create failed:%ld\n", PTR_ERR(dev_cls));
        ret = PTR_ERR(dev_cls);
        goto err2;
    }
    // 5、创建设备结点,在dev目录下,创建名称为DEV_NAME+i的设备结点;
    for(i=0;i<3;i++){
        devp = device_create(dev_cls, NULL, MKDEV(ma,i), NULL, "%s%d", DEV_NAME,i);
        if(IS_ERR(devp)){
            printk(KERN_ERR "device create failed: %ld\n", PTR_ERR(devp));
            ret = PTR_ERR(devp);
            goto err3;
        }
    }
    //2.1 初始化等待队列头。
    init_waitqueue_head(&wqh);  // 初始化等待队列头。
    
    printk(KERN_INFO "--%s--%s--%d----\n", __FILE__,__func__, __LINE__);
    
    return 0;
    
err3:
    for(--i;i>=0;i--){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
err2:
    cdev_del(mydev);
    
err1:
    unregister_chrdev_region(devNum, COUNT);

err0:
    return 0;
}
static void __exit mdev_exit(void)
{
    int i=0;
    printk("Good bye\n");
    for(i=0;i<3;i++){
        device_destroy(dev_cls,MKDEV(ma,i));
    }
    class_destroy(dev_cls);
    cdev_del(mydev);
    unregister_chrdev_region(devNum, COUNT);
}

module_init(mdev_init);
module_exit(mdev_exit);
MODULE_LICENSE("GPL");
View Code

 

posted @ 2022-05-03 17:47  笑不出花的旦旦  阅读(27)  评论(0编辑  收藏  举报