• 5.字符设备驱动------同步互斥阻塞


    引入 

    当设备被一个程序打开时,存在被另一个程序打开的可能,如果两个或多个程序同时对设备文件进行写操作,这就是说我们的设备资源同时被多个进程使用,对共享资源(硬件资源、和软件上的全局变量、静态变量等)的访问则很容易导致竞态。

    显然这不是我们想要的,所以本节引入互斥的概念:实现同一时刻,只能一个应用程序使用驱动程序

    互斥其实现很简单,就是采用一些标志,当文件被一个进程打开后,就会设置该标志,使其他进程无法打开设备文件

    目的

    同一时刻,只允许驱动程序被一个进程打开

    1.其中的标志需要使用函数来操作,不能直接通过判断变量来操作标志

    eg:

    open中记录打开的次数,can_open_cnt表示运行打开的次数,定义为1表示只允许一个设备打开.

    但是在汇编中一个c代码的加减实际上是分为a.读----b.修改----c.写回的过程,

    当A程序执行上述的a语句时,切换到B程序执行此程序,也执行到此语句的a.读,

    由于A并未修改can_open_cnt = 0,B也能正常打开,B关闭后,can_open_cnt++ = 1

    继续执行A。由此看来,使用判断变量来操作标志存在篡改的过程。

    1 int can_open_cnt =1;
    2 // open
    3 if (--can_open_cnt != 0) //先减后判断
    4 {  //0-->无进程访问驱动, <0 -->已有进程访问
    5     can_open_cnt++;
    6     return _EBUSY
    7 }
    8 //close 
    9 can_open_cnt++;

    1.1所以采用某种函数来实现,保证执行过程不被其他行为打断,有两种类型函数可以实现:

      原子操作(像原子一样不可再细分不可被中途打断)

    当多个进程同时访问同一个驱动时,只能有一个进程访问成功,其它进程会退出

      互斥信号量操作

    比如:A、B进程同时访问同一个驱动时,只有A进程访问成功了,B进程进入休眠等待状态,当A进程执行完毕释放后,等待状态的B进程又来访问,保证一个一个进程都能访问

    2. 原子操作详解

    1 常用原子操作函数举例:
    2 atomic_t v = ATOMIC_INIT(0);     //定义原子变量v并初始化为0
    3 atomic_read(atomic_t *v);        //返回原子变量的值
    4 void atomic_inc(atomic_t *v);    //原子变量增加1
    5 void atomic_dec(atomic_t *v);    //原子变量减少1
    6 int atomic_dec_and_test(atomic_t *v); //自减操作后测试其是否为0,为0则返回true,否则返回false。

    2.1修改驱动程序

    定义原子变量:

    1 /*定义原子变量canopen并初始化为1 */
    2 atomic_t canopen = ATOMIC_INIT(1); 

    在.open成员函数里添加:

    1 /*自减操作后测试其是否为0,为0则返回true,否则返回false   */
    2 if(!atomic_dec_and_test(&canopen))     
    3  {
    4   atomic_inc(&canopen);       //++,复位
    5   return -1;
    6  }

    在. release成员函数里添加:

     atomic_inc(&canopen);       //++,复位

    驱动程序:

      1 #include <linux/module.h>
      2 #include <linux/kernel.h>
      3 #include <linux/fs.h>
      4 #include <linux/init.h>
      5 #include <linux/delay.h>
      6 #include <linux/irq.h>
      7 #include <asm/uaccess.h>
      8 #include <asm/irq.h>
      9 #include <asm/io.h>
     10 #include <asm/arch/regs-gpio.h>
     11 #include <asm/hardware.h>
     12 #include <linux/poll.h>
     13 
     14 
     15 static struct class *sixthdrv_class;
     16 static struct class_device    *sixthdrv_class_dev;
     17 
     18 volatile unsigned long *gpfcon;
     19 volatile unsigned long *gpfdat;
     20 
     21 volatile unsigned long *gpgcon;
     22 volatile unsigned long *gpgdat;
     23 
     24 
     25 static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
     26 
     27 /* 中断事件标志, 中断服务程序将它置1,sixth_drv_read将它清0 */
     28 static volatile int ev_press = 0;
     29 
     30 static struct fasync_struct *button_async;
     31 
     32 
     33 struct pin_desc{
     34     unsigned int pin;
     35     unsigned int key_val;
     36 };
     37 
     38 
     39 /* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
     40 /* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
     41 static unsigned char key_val;
     42 
     43 struct pin_desc pins_desc[4] = {
     44     {S3C2410_GPF0, 0x01},
     45     {S3C2410_GPF2, 0x02},
     46     {S3C2410_GPG3, 0x03},
     47     {S3C2410_GPG11, 0x04},
     48 };
     49 
     50 static atomic_t canopen = ATOMIC_INIT(1);    //定义原子变量并初始化为1
     51 //static DECLARE_MUTEX(button_lock);     //定义互斥锁
     52 
     53 /*
     54   * 确定按键值
     55   */
     56 static irqreturn_t buttons_irq(int irq, void *dev_id)
     57 {
     58     struct pin_desc * pindesc = (struct pin_desc *)dev_id;
     59     unsigned int pinval;
     60     
     61     pinval = s3c2410_gpio_getpin(pindesc->pin);
     62 
     63     if (pinval)
     64     {
     65         /* 松开 */
     66         key_val = 0x80 | pindesc->key_val;
     67     }
     68     else
     69     {
     70         /* 按下 */
     71         key_val = pindesc->key_val;
     72     }
     73 
     74     ev_press = 1;                  /* 表示中断发生了 */
     75     wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
     76     
     77     kill_fasync (&button_async, SIGIO, POLL_IN);
     78     
     79     return IRQ_RETVAL(IRQ_HANDLED);
     80 }
     81 
     82 static int sixth_drv_open(struct inode *inode, struct file *file)
     83 {
     84     
     85     if (!atomic_dec_and_test(&canopen))    //自减操作
     86     {
     87         atomic_inc(&canopen);    //原子变量加1
     88         return -EBUSY;
     89     }    
     90 
     91     /* 配置GPF0,2为输入引脚 */
     92     /* 配置GPG3,11为输入引脚 */
     93     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
     94     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
     95     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
     96     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);    
     97 
     98     return 0;
     99 }
    100 
    101 ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
    102 {
    103     if (size != 1)
    104         return -EINVAL;
    105 
    106     if (file->f_flags & O_NONBLOCK)
    107     {
    108         if (!ev_press)
    109             return -EAGAIN;
    110     }
    111     else
    112     {
    113         /* 如果没有按键动作, 休眠 */
    114         wait_event_interruptible(button_waitq, ev_press);
    115     }
    116 
    117     /* 如果有按键动作, 返回键值 */
    118     copy_to_user(buf, &key_val, 1);
    119     ev_press = 0;
    120     
    121     return 1;
    122 }
    123 
    124 
    125 int sixth_drv_close(struct inode *inode, struct file *file)
    126 {
    127     atomic_inc(&canopen);    //原子变量加1
    128     free_irq(IRQ_EINT0, &pins_desc[0]);
    129     free_irq(IRQ_EINT2, &pins_desc[1]);
    130     free_irq(IRQ_EINT11, &pins_desc[2]);
    131     free_irq(IRQ_EINT19, &pins_desc[3]);
    132     //up(&button_lock);
    133     return 0;
    134 }
    135 
    136 static unsigned sixth_drv_poll(struct file *file, poll_table *wait)
    137 {
    138     unsigned int mask = 0;
    139     poll_wait(file, &button_waitq, wait); // 不会立即休眠
    140 
    141     if (ev_press)
    142         mask |= POLLIN | POLLRDNORM;
    143 
    144     return mask;
    145 }
    146 
    147 static int sixth_drv_fasync (int fd, struct file *filp, int on)
    148 {
    149     printk("driver: sixth_drv_fasync
    ");
    150     return fasync_helper (fd, filp, on, &button_async);
    151 }
    152 
    153 
    154 static struct file_operations sencod_drv_fops = {
    155     .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    156     .open    =  sixth_drv_open,     
    157     .read     =    sixth_drv_read,       
    158     .release =  sixth_drv_close,
    159     .poll    =  sixth_drv_poll,
    160     .fasync     =  sixth_drv_fasync,
    161 };
    162 
    163 
    164 int major;
    165 static int sixth_drv_init(void)
    166 {
    167     major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);
    168 
    169     sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");
    170 
    171     sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
    172 
    173     gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    174     gpfdat = gpfcon + 1;
    175 
    176     gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
    177     gpgdat = gpgcon + 1;
    178 
    179     return 0;
    180 }
    181 
    182 static void sixth_drv_exit(void)
    183 {
    184     unregister_chrdev(major, "sixth_drv");
    185     class_device_unregister(sixthdrv_class_dev);
    186     class_destroy(sixthdrv_class);
    187     iounmap(gpfcon);
    188     iounmap(gpgcon);
    189     return 0;
    190 }
    191 
    192 
    193 module_init(sixth_drv_init);
    194 
    195 module_exit(sixth_drv_exit);
    196 
    197 MODULE_LICENSE("GPL");
    sixth_drv.c

    测试程序:

     1 #include <sys/types.h>
     2 #include <sys/stat.h>
     3 #include <fcntl.h>
     4 #include <stdio.h>
     5 #include <poll.h>
     6 #include <signal.h>
     7 #include <sys/types.h>
     8 #include <unistd.h>
     9 #include <fcntl.h>
    10 
    11 
    12 /* sixthdrvtest 
    13   */
    14 int fd;
    15 
    16 void my_signal_fun(int signum)
    17 {
    18     unsigned char key_val;
    19     read(fd, &key_val, 1);
    20     printf("key_val: 0x%x
    ", key_val);
    21 }
    22 
    23 int main(int argc, char **argv)
    24 {
    25     unsigned char key_val;
    26     int ret;
    27     int Oflags;
    28 
    29     //signal(SIGIO, my_signal_fun);
    30     
    31     //fd = open("/dev/buttons", O_RDWR | O_NONBLOCK);
    32     fd = open("/dev/buttons", O_RDWR);
    33     if (fd < 0)
    34     {
    35         printf("can't open!
    ");
    36         return -1;
    37     }
    38 
    39     //fcntl(fd, F_SETOWN, getpid());
    40     
    41     //Oflags = fcntl(fd, F_GETFL); 
    42     
    43     //fcntl(fd, F_SETFL, Oflags | FASYNC);
    44 
    45 
    46     while (1)
    47     {
    48         ret = read(fd, &key_val, 1);
    49         printf("key_val: 0x%x, ret = %d
    ", key_val, ret);
    50         sleep(5);
    51     }
    52     
    53     return 0;
    54 }
    sixthdrvtest.c

    如下图,可以看到第一个进程访问驱动成功,后面的就再也不能访问成功了

    3.互斥信号量详解

    互斥信号量(semaphore)是用于保护临界区的一种常用方法,

    应用程序在操作之前,要先申请信号量,申请不到的话,要么返回,要么等休眠

    如果能申请到信号量,进程才能继续操作,操作完毕后,要释放信号量。

    释放了之后,如果有其他的应用程序等待获取此信号量的话,就要去唤醒那个应用程序。

     3.1信号量函数如下:

    /*注意: 在2.6.36版本后这个函数DECLARE_MUTEX修改成DEFINE_SEMAPHORE了*/
    1)static DECLARE_MUTEX(button_lock);         //定义互斥锁button_lock,被用来后面的down和up用
    
    2)void down(struct semaphore * sem);            // 获取不到就进入不被中断的休眠状态(down函数中睡眠)
    
    3)int down_interruptible(struct semaphore * sem);  //获取不到就进入可被中断的休眠状态(down函数中睡眠)
    
    4)int down_trylock(struct semaphore * sem);       //试图获取信号量,获取不到则立刻返回正数
    
    5)void up(struct semaphore * sem);               //释放信号量

    3.2修改驱动程序(以down函数获取为例)

     (1)定义互斥锁变量:

    /*定义互斥锁button_lock,被用来后面的down()和up()使用 */
    static DECLARE_MUTEX(button_lock); 

    (2)在.open成员函数里添加:

    /* 获取不到就进入不被中断的休眠状态(down函数中睡眠) */
              down(&button_lock);

    (3)在. release成员函数里添加:

     /*         释放信号量          */
              up(&button_lock);     

    驱动程序如下:

      1 #include <linux/module.h>
      2 #include <linux/kernel.h>
      3 #include <linux/fs.h>
      4 #include <linux/init.h>
      5 #include <linux/delay.h>
      6 #include <linux/irq.h>
      7 #include <asm/uaccess.h>
      8 #include <asm/irq.h>
      9 #include <asm/io.h>
     10 #include <asm/arch/regs-gpio.h>
     11 #include <asm/hardware.h>
     12 #include <linux/poll.h>
     13 
     14 
     15 static struct class *sixthdrv_class;
     16 static struct class_device    *sixthdrv_class_dev;
     17 
     18 volatile unsigned long *gpfcon;
     19 volatile unsigned long *gpfdat;
     20 
     21 volatile unsigned long *gpgcon;
     22 volatile unsigned long *gpgdat;
     23 
     24 
     25 static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
     26 
     27 /* 中断事件标志, 中断服务程序将它置1,sixth_drv_read将它清0 */
     28 static volatile int ev_press = 0;
     29 
     30 static struct fasync_struct *button_async;
     31 
     32 
     33 struct pin_desc{
     34     unsigned int pin;
     35     unsigned int key_val;
     36 };
     37 
     38 
     39 /* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
     40 /* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
     41 static unsigned char key_val;
     42 
     43 struct pin_desc pins_desc[4] = {
     44     {S3C2410_GPF0, 0x01},
     45     {S3C2410_GPF2, 0x02},
     46     {S3C2410_GPG3, 0x03},
     47     {S3C2410_GPG11, 0x04},
     48 };
     49 
     50 static DECLARE_MUTEX(button_lock);    //定义互斥锁
     51 /*
     52  * 确定按键值
     53  */
     54 static irqreturn_t buttons_irq(int irq, void *dev_id)
     55 {
     56     struct pin_desc * pindesc = (struct pin_desc *)dev_id;
     57     unsigned int pinval;
     58     
     59     pinval = s3c2410_gpio_getpin(pindesc->pin);
     60 
     61     if (pinval)
     62     {
     63         /* 松开 */
     64         key_val = 0x80 | pindesc->key_val;
     65     }
     66     else
     67     {
     68         /* 按下 */
     69         key_val = pindesc->key_val;
     70     }
     71 
     72     ev_press = 1;                  /* 表示中断发生了 */
     73     wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
     74     
     75     kill_fasync (&button_async, SIGIO, POLL_IN);
     76     
     77     return IRQ_RETVAL(IRQ_HANDLED);
     78 }
     79 
     80 static int sixth_drv_open(struct inode *inode, struct file *file)
     81 {
     82 #if 0    
     83     if (!atomic_dec_and_test(&canopen))    //自减操作
     84     {
     85         atomic_inc(&canopen);    //原子变量加1
     86         return -EBUSY;
     87     }    
     88 #endif
     89     /*获取信号量*/
     90     down(&button_lock);
     91     //若为第一次执行,则会获得信号量,第二次打开的话,就会进入休眠
     92 
     93     /* 配置GPF0,2为输入引脚 */
     94     /* 配置GPG3,11为输入引脚 */
     95     request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
     96     request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
     97     request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
     98     request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);    
     99 
    100     return 0;
    101 }
    102 
    103 ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
    104 {
    105     if (size != 1)
    106         return -EINVAL;
    107 
    108     if (file->f_flags & O_NONBLOCK)
    109     {
    110         if (!ev_press)
    111             return -EAGAIN;
    112     }
    113     else
    114     {
    115         /* 如果没有按键动作, 休眠 */
    116         wait_event_interruptible(button_waitq, ev_press);
    117     }
    118 
    119     /* 如果有按键动作, 返回键值 */
    120     copy_to_user(buf, &key_val, 1);
    121     ev_press = 0;
    122     
    123     return 1;
    124 }
    125 
    126 
    127 int sixth_drv_close(struct inode *inode, struct file *file)
    128 {
    129     free_irq(IRQ_EINT0, &pins_desc[0]);
    130     free_irq(IRQ_EINT2, &pins_desc[1]);
    131     free_irq(IRQ_EINT11, &pins_desc[2]);
    132     free_irq(IRQ_EINT19, &pins_desc[3]);
    133     up(&button_lock);    /*释放信号量*/
    134     return 0;
    135 }
    136 
    137 static unsigned sixth_drv_poll(struct file *file, poll_table *wait)
    138 {
    139     unsigned int mask = 0;
    140     poll_wait(file, &button_waitq, wait); // 不会立即休眠
    141 
    142     if (ev_press)
    143         mask |= POLLIN | POLLRDNORM;
    144 
    145     return mask;
    146 }
    147 
    148 static int sixth_drv_fasync (int fd, struct file *filp, int on)
    149 {
    150     printk("driver: sixth_drv_fasync
    ");
    151     return fasync_helper (fd, filp, on, &button_async);
    152 }
    153 
    154 
    155 static struct file_operations sencod_drv_fops = {
    156     .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    157     .open    =  sixth_drv_open,     
    158     .read     =    sixth_drv_read,       
    159     .release =  sixth_drv_close,
    160     .poll    =  sixth_drv_poll,
    161     .fasync     =  sixth_drv_fasync,
    162 };
    163 
    164 
    165 int major;
    166 static int sixth_drv_init(void)
    167 {
    168     major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);
    169 
    170     sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");
    171 
    172     sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
    173 
    174     gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    175     gpfdat = gpfcon + 1;
    176 
    177     gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
    178     gpgdat = gpgcon + 1;
    179 
    180     return 0;
    181 }
    182 
    183 static void sixth_drv_exit(void)
    184 {
    185     unregister_chrdev(major, "sixth_drv");
    186     class_device_unregister(sixthdrv_class_dev);
    187     class_destroy(sixthdrv_class);
    188     iounmap(gpfcon);
    189     iounmap(gpgcon);
    190     return 0;
    191 }
    192 
    193 
    194 module_init(sixth_drv_init);
    195 
    196 module_exit(sixth_drv_exit);
    197 
    198 MODULE_LICENSE("GPL");
    sixdrv2.c

    测试程序同上一个。

    测试:

    下图,只有831进程在处于静止状态(没有中断触发)

    832进程处于down休眠状态,直至831释放掉信号

    灭掉进程831

    多个信号量访问时, 会一个一个进程来排序访问

    4.阻塞与非阻塞

    4.1概念

    1. 阻塞,

        如果不满足条件会挂起,直到满足可操作的条件后再进行操作。

        被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。

        2. 非阻塞,

        不满足条件返回,进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止

    4.2 怎么来判断阻塞与非阻塞操作?

    1.在用户层open时,默认为阻塞操作,如果添加了” O_NONBLOCK”,表示使open()、read()、write()不被阻塞

    实例:

    fd = open("/dev/buttons",O_RDWR);    //使用阻塞操作
    fd = open("/dev/buttons ", O_RDWR | O_NONBLOCK);   //非阻塞操作   

    2.然后在驱动设备中,通过file_operations成员函数.open、.read、.write带的参数file->f_flags 来查看用户层访问时带的参数

    实例:

    1  if(  file->f_flags & O_NONBLOCK )   //非阻塞操作,获取不到则退出
    2   {
    3      ... ...
    4   }
    5   else   //阻塞操作,获取不到则进入休眠
    6   {
    7      ... ...
    8   

    4.3 修改应用程序,通过判断file->f_flags来使用阻塞操作还是非阻塞操作

    (1)定义互斥锁变量:

    /*定义互斥锁button_lock,被用来后面的down()和up()使用 */
    static DECLARE_MUTEX(button_lock); 

    (2)在.open成员函数里添加:

    if( file->f_flags & O_NONBLOCK )   //非阻塞操作
      {
       if(down_trylock(&button_lock) )       //尝试获取信号量,获取不到则退出
                return -1;
      }
    else   //阻塞操作
      {
         down(&button_lock);         //获取信号量,获取不到则进入休眠
      }

    (3)在read成员函数里添加

    ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
    {
        if (size != 1)
            return -EINVAL;
    
        if (file->f_flags & O_NONBLOCK)
        {    /*非阻塞*/
            if (!ev_press)
                return -EAGAIN;
        }
        else
        {    /*阻塞*/
            /* 如果没有按键动作, 休眠 */
            wait_event_interruptible(button_waitq, ev_press);
        }
    
        /* 如果有按键动作, 返回键值 */
        copy_to_user(buf, &key_val, 1);
        ev_press = 0;
        
        return 1;
    }

     (4)在. release成员函数里添加:

      /*释放信号量*/
      up(&button_lock);     

    测试阻塞:

     1 int main(int argc,char **argv)
     2 {
     3   int oflag;
     4   unsigned int val=0;       
     5   fd=open("/dev/buttons",O_RDWR);           //使用阻塞操作
     6   if(fd<0)
     7        {printf("can't open, fd=%d
    ",fd);      
     8        return -1;}
     9   else
    10        {
    11        printf("can open,PID=%d
    ",getpid());    //打开成功,打印pid进程号
    12        } 
    13 
    14    while(1)
    15    { 
    16     val=read( fd, &ret, 1);              //读取驱动层数据
    17      printf("key_vale=0X%x,retrun=%d
    ",ret,val);  
    18    }
    19    return 0;

    测试非阻塞:

    上面的测试程序

    第5行:

    fd=open("/dev/buttons",O_RDWR | O_NONBLOCK);

    参考

    .按键之互斥、阻塞机制(详解)

  • 相关阅读:
    2019 SDN第六次作业
    第07组 Beta冲刺(2/4)
    第07组 Beta冲刺(1/4)
    2019 SDN第5次作业
    SDN课程阅读作业(2)
    第08组 Beta版本演示
    第08组 Beta冲刺(4/4)
    第08组 Beta冲刺(3/4)
    第08组 Beta冲刺(2/4)
    第08组 Beta冲刺(1/4)
  • 原文地址:https://www.cnblogs.com/y4247464/p/10113869.html
Copyright © 2020-2023  润新知