Linux/Android系統知識之Linux入門篇--編寫Linux驅動

前言

由於通用性強,就業面廣,源碼免費等原因,Linux近些年火遍了大江南北,大到雲服務器,小到路由器,無處不見Linux的身影。知乎上各種linux書籍推薦的也是琳琅滿目,《ldd3》、《內線源代碼情景分析》、《深入理解Linux內核》等等,讓有選擇困難症的朋友犯了難。

學習Linux的朋友,首先必須要建立這樣一個觀念:學習Linux驅動和學習Linux內核是兩碼事情,Linux內核提供了各種現成的接口供驅動開發者直接使用,驅動編寫者只需學習linux驅動運作的規則和概念,便可編寫自己的驅動用例了。

這就好比在Windows上開始寫第一個”hello world程序”時,書上也只是教我們先敲入如下代碼:

#include<stdio.h>
void main()
{
    printf("hello world\n");
}

然後點擊Visio studio的編譯按鈕即可編譯運行生成的exe文件一樣,Linux driver編寫只要遵循它的條條框框,便可寫出我們的第一個驅動代碼了。

編寫第一個驅動文件

#include <linux/init.h>  
#include <linux/sched.h>  
#include <linux/module.h>  
#include <linux/kernel.h>  
#include <linux/fs.h>    
#include <linux/cdev.h>  
#include <linux/device.h>  
#include <linux/ioctl.h>    

static int __init hello_init(void)  
{  
        printk("hello, init\n");  
        return 0;  
}  

static void __exit hello_exit(void)  
{  
        printk("hello, exit\n");  
}  

module_init(hello_init);  
module_exit(hello_exit);  

MODULE_LICENSE("GPL");  
MODULE_AUTHOR("happybevis");  

上面是我們的第一個linux驅動代碼

  • #include \

#define KERN_EMERG      "<0>" /* system is unusable */
#define KERN_ALERT      "<1>" /* action must be taken immediately */
#define KERN_CRIT       "<2>" /* critical conditions */
#define KERN_ERR        "<3>" /* error conditions */
#define KERN_WARNING    "<4>" /* warning conditions */
#define KERN_NOTICE     "<5>" /* normal but significant condition */
#define KERN_INFO       "<6>" /* informational */
#define KERN_DEBUG      "<7>" /* debug-level messages */

printk函數中可以加入打印機級別的宏,該例中沒加,系統會自動爲其補上“KERN_WARNING”的級別。所以我們也可以這樣使用printk函數:printk(KERN_INFO “Hello, world!\n”);

要查看我們前面打印的log,需要使用命令行中的dmesg命令,該命令將會打印所以使用printk函數丟出的kernel log。

編譯驅動代碼

寫完了驅動代碼,我們應該如何去編譯和使用呢?

如果你使用的是ubuntu等操作系統,請先使用uname命令查看內核版本

deployer@iZ28v0x9rjtZ:~$ uname  -r
3.2.0-67-generic

這裏本地電腦的內核版本是3.2.0,linux based操作系統會在“/lib/modules/(內核版本號)/build”下提供本機內核模塊所需的所有編譯環境。

若有朋友想進行移植或更深入的瞭解內核運行原理,可以在Linux官網下載到想要進行研究或移植的Linux對應版本源碼了,這些源碼通過與目標平臺對應交叉編譯工具,可自行完整編譯移植。

源碼下載官網地址爲Kernel,想查找對應版本的源碼可到如下對應文件夾中對應查找即可例如3.2源碼下載.

首先編寫我們的Makefile(編譯規則文件),路徑大家可以隨意,我是放在~/kernel_test/test目錄。

  obj-m := hello.o  

  PWD  := $(shell pwd)
  KVER := $(shell uname -r)
  KDIR := /lib/modules/$(filter-out ' ',$(KVER))/build
  all:  
          $(MAKE) -C $(KDIR) M=$(PWD) modules  
  clean:  
          rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions  

特別注意:all和clean語句的下一句一定要以tab鍵開頭,否則Makefile語法會報錯。

將前面編寫的hello.c文件也拷貝到test文件夾中,直接調用make命令開始編譯:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ make
make -C /lib/modules/3.2.0-67-generic/build   M=/home/deployer/kernel_test/test   modules  
make[1]: Entering directory `/usr/src/linux-headers-3.2.0-67-generic'
  Building modules, stage 2.
  MODPOST 0 modules
make[1]: Leaving directory `/usr/src/linux-headers-3.2.0-67-generic'

編譯成功。

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ls
hello.c  hello.ko  hello.mod.c  hello.mod.o  hello.o  Makefile  modules.order  Module.symvers

生成了一大堆文件,我們真正需要的是hello.ko,即內核模塊,也就是我們的驅動模塊啦~
安裝和卸載和查看內核模塊需要用到如下三個命令:

  1. lsmod:查看目前系統中安裝過的內核模塊。
  2. insmod:安裝內核模塊。
  3. rmmod:卸載內核模塊。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ lsmod
Module                  Size  Used by
joydev                 17693  0 
xen_kbdfront           12797  0 
fb_sys_fops            12703  0 
sysimgblt              12806  0 
sysfillrect            12901  0 
syscopyarea            12633  0 
usbhid                 47238  0 
hid                    99883  1 usbhid
i2c_piix4              13301  0 
psmouse                98051  0 
serio_raw              13211  0 
mac_hid                13253  0 
lp                     17799  0 
parport                46562  1 lp
xenfs                  18311  1 
floppy                 70207  0 

我們開始安裝內核驅動:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ insmod hello.ko
insmod: error inserting 'hello.ko': -1 Operation not permitted

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo insmod hello.ko

因爲驅動代碼運行在內核空間,若編寫過程引入重大bug,有可能導致Linux內核crash,操作系統重啓。所以安裝和卸載內核代碼均需要管理員權限。有興趣的朋友可以在虛擬機中故意編寫一個帶空指針代碼的驅動,看看效果怎樣。

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg  |grep -i hello
[2908464.238822] hello, init

可以看到,我們的hello_init函數被成功調用。

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ lsmod
Module                  Size  Used by
hello                  12425  0 
joydev                 17693  0 
xen_kbdfront           12797  0 
fb_sys_fops            12703  0 
sysimgblt              12806  0 
sysfillrect            12901  0 
syscopyarea            12633  0 
usbhid                 47238  0 
hid                    99883  1 usbhid
i2c_piix4              13301  0 
psmouse                98051  0 
serio_raw              13211  0 
mac_hid                13253  0 
lp                     17799  0 
parport                46562  1 lp
xenfs                  18311  1 
floppy                 70207  0 

內核模塊也已成功加載。

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo rmmod hello.ko
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg  |grep -i hello
[2908464.238822] hello, init
[2908491.639216] hello, exit

卸載我們的驅動後,hello_exit函數也被成功調用。由此相信大家對內核驅動的調用過程有了較深刻的理解。

與內核驅動進行交互

前面編寫的驅動代碼,本質上只是打印了幾個log,並無其他實質性操作。作爲一個”可用”的驅動,一般需要提供對外接口以供同外界進行數據交互和控制。

在Linux系統中,一切皆文件。內核驅動可以通過調用一些的函數,讓操作系統建立一些文件節點,這些文件節點在用戶看來完全就是一個普通文件,我們通過對該特殊文件的讀寫和ioctl操作即可實現數據的傳遞了。

deployer@iZ28v0x9rjtZ:~$ ll 
drwxr-xr-x  2 root root          60 Jun 26 00:08 cpu/
crw-------  1 root root      1,  11 Jun 26 00:08 kmsg
-rw-r--r--  1 root     root       156 Jul 30 06:00 run_jobs.log
brw-rw---- 1 root disk 7, 7 Jun 26 00:08 /dev/loop7
...

我們可以通過ls -l命令看出一個文件是否爲真實文件,只需要關注第一個字符:“c” –> 字符設備驅動(char);“b” –> 塊設備驅動(block);“d” –> 普通目錄(directory); “ - ” –> 普通的文件(file).

我們常用如下四種方式創建文件節點以供交互:

  1. proc 常用來製作簡易的參數查看節點,由於sys文件系統功能與之十分類似且sys更新更靈活,所以近來proc使用的越來越少。
  2. sys 常用來和控制驅動的運行參數。
  3. dev 。
  4. debugfs (該方式一般用來提供debug調試接口,正常的驅動不太會提供,所以會跳過其講解,有興趣的朋友請再自行學習)
deployer@iZ28v0x9rjtZ:~$ ls /
bin   dev  home lib lost+found  mnt  proc  run  selinux  sys  usr  vmlinuz
boot  etc  initrd.img  lib64  media opt  root  sbin  srv   tmp  var

查看系統根目錄,很容易看到sys、proc、dev三個文件夾,分別調用前面所講的三種創建文件系統的函數,操作系統便會幫我們分別在如下三個文件夾中建立對應的文件節點了。

建立dev文件文件節點

在我們已經寫好的驅動文件中稍做修改:

#include <linux/init.h>  
#include <linux/sched.h>  
#include <linux/module.h>  
#include <linux/kernel.h>  
#include <linux/fs.h>    
#include <linux/cdev.h>  
#include <linux/device.h>  
#include <linux/ioctl.h>   
#include <linux/miscdevice.h> 
#include <linux/uaccess.h>

    int hello_open(struct inode *inode, struct file *filp)  
    {  
            printk("hello open!\n");  
            return 0;   
    }  

    int hello_release(struct inode *inode, struct file *filp)  
    {  
            printk("hello release!\n");  
            return 0;  
    }  

    ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)  
    {  
        printk("hello read!\n");  
        return 0;  
    }  

    ssize_t hello_write(struct file *filp, char __user *buf, size_t count, loff_t *fpos)  
    {  
        printk("hello write!\n");  
        return 0;  
    }  

    int hello_ioctl(struct inode *inode, struct file *filp,  
            unsigned int cmd, unsigned long arg)  
    {  
        printk("hello ioctl!\n");  
        printk("cmd:%d arg:%d\n", cmd, arg);  
        return 0;  
    }  


    struct file_operations fops =   
    {  
        .owner      =   THIS_MODULE,  
        .open       =   hello_open,  
        .release    =   hello_release,  
        .write      =   hello_write,  
        .read       =   hello_read,  
        .unlocked_ioctl      =   hello_ioctl  
    };    

    struct miscdevice dev =   
    {  
        .minor  =   MISC_DYNAMIC_MINOR,  
        .fops    =   &fops,  
        .name   =   "hello_device"
    };    

    int setup_hello_device(void)  
    {  

        return misc_register(&dev);  
    }   

    static int __init hello_init(void)  
    {  
            printk("hello, init\n");  
            return setup_hello_device();  
            return 0;  
    }  

    static void __exit hello_exit(void)  
    {  
            printk("hello, exit\n");  
        misc_deregister(&dev);
    }  

    module_init(hello_init);  
    module_exit(hello_exit);  

    MODULE_LICENSE("GPL");  
    MODULE_AUTHOR("happybevis"); 

接下來make driver模塊後,安裝我們的驅動模塊。

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ls /dev/h*
hello_device  hidraw0       hpet 

可以看到dev目錄下產生了我們建立的”hello_device”文件節點了~

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg  |grep -i hello
[2980042.371331] hello, init

接下來我們像讀取文件一下讀一下驅動節點內容

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo cat /dev/hello_device
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg  |grep -i hello
[2980042.371331] hello, init
[2980380.028918] hello open!
[2980380.028935] hello read!
[2980380.028938] hello release!

可見cat命令會幫我們做open、read、close三種操作。由於我們在kernel的read函數中只是打印了一個log,並沒有真正傳遞數據,所以cat不到內容。

接下來我們再稍作修改,在驅動的read函數中返回一個字符串給用戶(文件讀取者)

ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)  
       {
          char *hellobuff = "hello world\n" ;
          loff_t position = *pos; 

          if (position >= 15) {
                  count = 0;
                  goto out; 
          }       

          if (count > ( 15 - position ))
                  count = 15 - position;
          position += count; 

          printk("hello read!\n");
          if( copy_to_user( buf, hellobuff + *pos , count ) ){
                  count =  -EFAULT;
                  goto out;
          }       

          *pos = position;

          out:
                  return count;  
          } 

重新make install,測試我們的功能:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo cat /dev/hello_device 
hello world 

測試成功,這樣一來kernel和用戶之間就可以通過文件讀取和寫入來傳遞信息了。用戶空間和kernel空間的數據傳遞需要用到copy_to_user和copy_from_user函數,前者在driver的read函數中,將數據傳給用戶,後者用着write中獲取用戶傳入數據。

前面我們使用的是cat命令讀取driver數據,當然用echo “xxx”>/dev/hello_device就可以向driver傳入數據了。除此之外,我們當然可以自己寫linux的應用程序,程序中只需要調用正常的fopen、fread、fwrite、fclose函數組(open、rea的、write、close亦可)同樣可以和driver打交道了。

測試程序如下:
#include <fcntl.h>  
#include <stdio.h>  

char temp[64]={0};  
int main( void )  
{  
    int fd,len;  

    fd = open("/dev/hello_device",O_RDWR);  
    if(fd<0)  
    {  
        perror("open fail \n");  
        return ;  
    }   

    len=read(fd,temp,sizeof(temp));  

    printf("len=%d,%s \n",len,temp);  

    close(fd);  
}  
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ gcc test.c  -o test

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ./test 
open fail 
: Permission denied

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo ./test 
len=15,hello world

可以看到我們的driver內容就正常被讀取到啦。

除了read、write的文件操作外linux應用程序中還有ioctl操作,具體細節可以使用man ioctl命令查看,我們只需在代碼中加入ioctl( fd, para, &returnbuff );就可以同我們driver中的hello_ioctl函數進行交互了。

正常情況下由於我們自定義的ioctl para會比較多,所以一般driver中寫法如下:

static long hello_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)  
{  
    int err = 0;  
    int returnbuff = 0 ;
    printk("hello ioctl!\n");  
     printk("cmd:%d arg:%d\n", cmd, arg);  
    switch(cmd)  
    {  
        case PARA1:  
            printk("case: PARA1/n");  
            // do something ... 
            break;  
        case PARA2:  
            printk("case: PARA2/n");  
            err = copy_to_user((int *)arg, &returnbuff, sizeof(int));  
            break;  
        case PARA3:  
            printk("case: PARA3/n");  
            err = copy_from_user(&returnbuff,(int *)arg, sizeof(int));  
            break;  
        default:  
            printk("case: default/n");  
            err = ENOTSUPP;  
            break;  
    }   
    return err;  
}  

所以ioctl比read write來講會更加靈活多變,代碼結構相對也會清晰一些。

下面我們簡單總結一下,用戶程序(shell命令或linux應用程序)可以通過常規的文件操作方式,通過驅動創建的文件節點進行交互。交互的方式一般是通過read、write、ioctl(dev文件節點才支援)來進行數據交換。如此一來kernel和現實世界就可以互相協作,互相溝通了~!

建立proc文件文件節點

proc文件節點由於歷史原因,在較早kernel版本中的使用方法和新版使用方式不同且互不兼容。由於舊版使用的地方已經很少,所以我們將要研究的是新用法。

繼續在我們driver的基礎上加入proc文件節點相關代碼:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ git diff hello.c
diff --git a/hello.c b/hello.c
index 9fda5e2..8a4d32a 100644
--- a/hello.c
+++ b/hello.c
@@ -8,7 +8,9 @@
 #include <linux/ioctl.h>   
 #include <linux/miscdevice.h> 
 #include <linux/uaccess.h> 
 #include <linux/slab.h>
+#include <linux/proc_fs.h> 
+#include <linux/seq_file.h>

        int hello_open(struct inode *inode, struct file *filp)  
        {  
@@ -60,8 +62,26 @@
            printk("hello ioctl!\n");  
            printk("cmd:%d arg:%d\n", cmd, arg);  
            return 0;  
       }  

+  
+       static int hello_proc_show(struct seq_file *m, void *v)
+       {
+               seq_printf(m, "hello world\n");
+               return 0;
+       }       
+
+       static int hello_proc_open(struct inode *inode, struct file *file)
+       {
+               return single_open(file, hello_proc_show, NULL);
+       }
+
+       static const struct file_operations hello_proc_fops = {
+       .owner          = THIS_MODULE,
+       .open           = hello_proc_open,
+       .read           = seq_read,
+       .llseek         = seq_lseek,
+       .release        = seq_release,
+       }; 

        struct file_operations fops =   
        {  
@@ -82,7 +102,8 @@

        int setup_hello_device(void)  
        {             
+           if (!proc_create("hello_proc", 0, NULL, &hello_proc_fops))
+               return -ENOMEM;     
            return misc_register(&dev);  
        }   

@@ -97,6 +118,7 @@
        {  
                printk("hello, exit\n");  
                misc_deregister(&dev);
+               remove_proc_entry("hello_proc", NULL);
        }  

        module_init(hello_init);  

編譯安裝我們的內核驅動,如果代碼沒問題的話,應該可以看到如下節點長出~

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /proc/hello_proc 
-r--r--r-- 1 root root 0 Jul 30 15:02 /proc/hello_proc

我們讀取該節點資訊如下:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ cat /proc/hello_proc 
hello world

需要說明一點,seq相關的操作是內核提供的操作,目的是爲了讓proc文件節點能夠突出更多地內容,舊版proc的操作接口很容易因開發人員使用不當導致交換的數據量大於copy_to_user所能支援的內存大小(若沒記錯應爲一個page大小)。對proc的使用者來說,直接套用如上框架即可保證使用的安全。

另外需要特別說明一點,proc文件操作結構中的.release = seq_release 域一定不要漏掉,否則每打開一次改proc節點就會產生一些內存泄漏,雖然泄漏量很少,積少成多內存遲早會爆掉。

建立sys文件文件節點

有了前面的經驗,很容易就想到sys fs文件節點的操作方式也大同小異,一般我們通過對sysfs節點的cat和echo操作,查看或改變driver的一些全局變量,從而對driver的狀況進行監控,對driver的行爲進行動態修改。

接下來我們先在sys目錄下簡歷一個名爲hello_sysfs_dir的文件夾,在其中建立一個可供讀寫的節點hello_node。
活不多說,說走就走。

在已經完成的proc driver基礎上,我們記憶添加sys fs操作節點
diff --git a/hello.c b/hello.c
index 8a4d32a..f61fafe 100644
--- a/hello.c
+++ b/hello.c
@@ -11,6 +11,7 @@
 #include <linux/slab.h>
 #include <linux/proc_fs.h> 
 #include <linux/seq_file.h>
+#include <linux/kobject.h>

        int hello_open(struct inode *inode, struct file *filp)  
        {  
@@ -63,6 +64,27 @@
            printk("cmd:%d arg:%d\n", cmd, arg);  
            return 0;  
        }
+
+       char buff[512] = {0} ;
+       int in_count = 0;
+
+       static ssize_t hello_sysfs_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf
+       {
+                //return sprintf(buf, "%s\n", "hello sysfs read\n");
+                return sprintf(buf, "[hello sysfs write] user data: length=0x%X,buff=%s\n",in_count
+       }
+
+       static ssize_t hello_sysfs_store(struct kobject *kobj, struct kobj_attribute *attr, const ch
+       {
+        printk("[hello sysfs write] user data: length=0x%X,buff=%s\n",count,buf);
+       in_count = count ;
+       strncpy(buff, buf , 512); 
+       if(count)
+               return count;
+        else
+               return 1 ;
+       }
+

        static int hello_proc_show(struct seq_file *m, void *v)
        {
@@ -98,12 +120,41 @@
            .minor  =   MISC_DYNAMIC_MINOR,  
            .fops    =   &fops,  
            .name   =   "hello_device"  
      }; 
+
+static struct kobj_attribute hello_attr = 
+               __ATTR(hello_node, 0666, hello_sysfs_show, hello_sysfs_store);
+ 
+static struct attribute *attrs [] = {
+               &hello_attr.attr,
+               NULL,
+       };  
+
+static struct attribute_group hello_attr_group = {
+       .attrs = attrs,
+       };

+struct kobject *dir = NULL;
+
+
        int setup_hello_device(void)  
       {   
+        int retval = 0 ; 
+         //--------proc fs part-------------- 
          if (!proc_create("hello_proc", 0, NULL, &hello_proc_fops))
                return -ENOMEM;     
+       
+       //--------sys fs part ----------------
+           dir = kobject_create_and_add("hello_sysfs_dir", NULL);
+            if (!dir)
+                 return -ENOSYS;
+               
+          retval = sysfs_create_group(dir, &hello_attr_group);
+            if (retval)
+               kobject_put(dir);
+           
+
+       //---------create a device(dev fs) part------------
            return misc_register(&dev);  
        }   

@@ -119,6 +170,7 @@
                printk("hello, exit\n");  
                misc_deregister(&dev);
                remove_proc_entry("hello_proc", NULL);
+               kobject_put(dir);
        }  

        module_init(hello_init); 

這樣一來我們向該節點寫入的字串,就可以通過cat的方式讀出驗證了。

馬上編譯安裝試試效果:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /sys/
total 4
drwxr-xr-x 14 root root    0 Jul 30  2017 ./
drwxr-xr-x 23 root root 4096 Mar  9  2015 ../
drwxr-xr-x  2 root root    0 Jul 30  2017 block/
drwxr-xr-x 21 root root    0 Jul 30  2017 bus/
drwxr-xr-x 44 root root    0 Jul 30  2017 class/
drwxr-xr-x  4 root root    0 Jul 30  2017 dev/
drwxr-xr-x 18 root root    0 Jul 30  2017 devices/
drwxr-xr-x  4 root root    0 Jul 30 15:48 firmware/
drwxr-xr-x  6 root root    0 Jul 30 15:48 fs/
drwxr-xr-x  2 root root    0 Jul 30 15:59 hello_sysfs_dir/
...
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /sys/hello_sysfs_dir/
total 0
drwxr-xr-x  2 root root    0 Jul 30 15:59 ./
drwxr-xr-x 14 root root    0 Jul 30  2017 ../
-rw-rw-rw-  1 root root 4096 Jul 30 15:59 hello_node

sysfs的文件節點完美的漲了出來,趕緊試試咱的功能:

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ echo "how are you" >/sys/hello_sysfs_dir/hello_node 

deployer@iZ28v0x9rjtZ:~/kernel_test/test$ cat /sys/hello_sysfs_dir/hello_node 
[hello sysfs write] user data: length=0xC,buff=how are you

結果如預期,大功告成!

小結

Linux驅動和用戶打交道的最主要方式,就是是其所建立各種文件節點(netlink etc..),本文以三種文件節點的創建方式爲主線,讓大家對Linux中一切皆爲文件的思想有更直觀的認識。 本文的所有細節都是一步步修改運行並返回的結果,所以建議大家有條件的話一定要完全按照文章步驟做一遍,看再多也不如親自做一遍領悟的更多。

driver是一門大學科,詳細的知識細節想必要一本書才能講得完,篇幅有限,僅本文中內容已有許多細節特意沒有展開,讀者若有疑惑或建議,歡迎一起討論完善。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章