资源描述
设备驱动程序二设备驱动程序二16、人民应该为法律而战斗,就像为了城墙而战斗一样。赫拉克利特17、人类对于不公正的行为加以指责,并非因为他们愿意做出这种行为,而是惟恐自己会成为这种行为的牺牲者。柏拉图18、制定法律法令,就是为了不让强者做什么事都横行霸道。奥维德19、法律是社会的习惯和思想的结晶。托伍威尔逊20、人们嘴上挂着的法律,其真实含义是财富。爱献生中断控制器寄存器操作中断控制器寄存器操作6中断源中断源7注册和注销中断响应函数注册和注销中断响应函数注册中断响应函数打开中断响应驱动程序工作关闭中断响应注销中断响应函数int register_irq(unsigned int irq,void(*handler)(int,void*,struct pt_regs*),unsigned long flags,const char*dev_name,void*dev_id);void free_irq(unsigned int irq,void*dev_id);request_irq()/free_irq()unsigned int irq 中断号,每个中断响应函数都对应一个中断号。中断号由使用的硬件决定。void(*handler)(int,void*,struct pt_regs*)指向中断响应函数的指针。unsinged long flags 用于中断控制的变量,实际是一些选项的位掩码。const char*dev_name 字符串指针。这个字符串将显示在/proc/interrupts中,表示这个中断的所有者。void*dev_id 这个指针用于共享的中断线。这个指针一般由驱动程序自己使用,指向它自己的私有数 据。在中断不共享时,这个指针可以是空指针,但一般将它指向设备自己。8flags中的位标志中的位标志SA_INTERRUPT 当这个比特置位时表示注册的是一个“快速”中断,即在中断响应期间中断是被禁止的。(相对于“慢速”中断,因为需要较长的处理时间,系统在处理“慢速”中断期间中断是 不被禁止的。这种中断一般很少被使用。)SA_SHIRQ 这个比特被置位时表示这个中断可以被其它设备共享。如果其他设备已经注册了这个中 断并且不是共享方式,则无法再注册这个中断的中断响应函数。SA_SAMPLE_RANDOM 这个比特被置位时表示这个中断可以对熵池做贡献。熵池是操作系统用于产生真随机数 的机制。熵池中的随机数经常被用来生成用于安全或加密的密码。如果这个中断的发生 是完全随机的,比如键盘中断,则可以将这个比特置位,向熵池贡献;否则,比如固定 间隔的时钟中断,不包含随机性,这种中断就用于为熵池做贡献。9使用使用register_irq()注册一个中断也是对系统资源申请的一个过程。注册成功则响应的系统资源将被占用。注册一个中断可以在驱动程序初始化的时候,也可以在设备第一次被打开的时候。一般,中断的注册在设备第一次被打开、同时设备硬件的中断功能还没有被使能时进行。这种方式需要驱动程序本身对设备的打开次数进行管理。int result;result=register_irq(SPIOC_IRQ,spioc_interrupt,SA_INTERRUPT,“spioc”,NULL);if(result)printk(KERN_INFO“spioc:cant get assigned irq%dn”,SPIOC_IRQ);return-1;else enable_irq();我们可以通过/proc界面查看系统上中断的使用情况:/proc/interrupts/proc/stat10中断号中断号中断号是一个中断最根本的标志,每个中断源对应一个中断号。中断号是唯一的,并且是由硬件决定的。我们要使用一个中断,首先就需要知道它的中断号。静态设定中断号:在我们写驱动程序时已经明确知道要使用的中断号。驱动程序模块加载时设定中断号:由用户在加载驱动程序时指定要使用的中断号。比如某些ISA卡通过跳线设定所使用的中断,这时用户可根据跳线的设置在加载驱动程序时指定中断号,如:insmod./spioc.o spioc_irq=x自动探测中断号:由驱动程序自动决定或由操作系统决定要使用的中断号。如PCI设备的中断号是在系统启动时由系统分配的。1 完全由驱动程序执行自动探测:根据一些设备的基本知识,驱动程序可以根据设备的某些参数判断应该使用哪个中断号。2 借助内核的帮助:内核提供了一套底层工具用于探测中断号unsigned long probe_irq_on(void);int probe_irq_off(unsigned long);11关于中断响应函数关于中断响应函数中断响应函数需要满足如下一些限制:-中断响应函数不能向用户空间或从用户空间传送数据,因为中断并不在进程环境中执行。-中断响应函数不能做任何与进入睡眠有关的操作,如调用sleep_on()等。-中断响应函数不能使用除GFP_ATOMIC类型之外的内存分配功能。-中断响应函数不能对一个信号量加锁。-中断响应函数中不能调用schedule()函数进行进程调度。一般,中断中断响应函数完成的工作包括:-从设备读数据,或向设备写数据。-简单的数据处理或其他简单操作。-唤醒一个在这个设备上睡眠的进程。12使用使用tasklettasklet是Linux中断处理中bottom-half机制的实现方式之一。在2.3版本的内核之前,Linux使用老式的bottom-half实现方式,称为BH。如果使用的是老版本的内核,则无法使用tasklet。tasklet实现了一个中断处理过程的下半部,因此它也要遵守中断响应函数应该遵守的限制。tasklet在一个中断的上半部运行完成后才能获得运行机会,并且由上半部进行调度。一个tasklet可以被多次调度,但只运行一次。要使用tasklet,需要首先使用DECLARE_TASKLET宏进行声明DECLARE_TASKLET(name,function,data);name:tasklet的名字。function:tasklet运行的函数的名字。它的参数类型为unsigned long,返回值类型为void。data:作为传递给function的数据,是unsigned long类型。然后,在中断上半部中用tasklet_schedule()来调度运行一个tasklet。13void spioc_do_tasklet(unsigned long);.DECLARE_TASKLET(spioc_tasklet,spioc_do_tasklet,0);./*The interrupt handler*/void spioc_interrupt(int irq,void*dev_id,struct pt_regs*regs)/*read/write data etc.*/.tasklet_schedule(&spioc_tasklet);./*other operations*/*The tasklet function*/void spioc_do_tasklet(unsigned long somevar)/*Here should do the not so critic but time consumingdata processing*/./*If some waiting queue is need to be waked up,do it here*/wake_up_interruptible(&spioc_read_queue);14竞争情况竞争情况由于中断出现的随机性,驱动程序在处理中断时要特别注意存在竞争条件的情况。避免竞争情况出现所采用的方法充满了各式各样的技巧,而且由于竞争情况本身的复杂性,避免竞争的方法难于全面分类和描述。一般,比较常用的方法有:1.使用循环缓冲区,不使用共享变量。2.使用自旋锁来实现互斥。3.使用可以自动增加/减少的锁变量。由于信号量的使用可能导致一个过程进入睡眠,所以在中断响应函数中不能使用信号量。关于竞争情况和处理方法,详见Linux设备驱动程序第九章的相关内容。15驱动程序的其它内容阻塞型输入输出:睡眠和唤醒内存使用:申请和分配时间控制:延迟和定时使用devfs自动获得主设备号驱动程序调式技术安全性16阻塞型输入输出阻塞型输入输出当一个进程从设备读数据但还没有可用的数据时,或向设备写数据而设备还没准备好时,进程一般应该进入睡眠。当有可用数据或设备准备就绪时,再通过唤醒的方式将睡眠中的进程唤醒,使得操作可以进行下去。等待队列及其初始化:wait_queue_head_t spioc_wait_queue;init_waitqueue_head(&spioc_wait_queue);还可以用如下方式声明一个静态的等待队列:DECLARE_WAIT_QUEUE_HEAD(spioc_wait_queue);上述语句声明的等待队列在编译时被初始化。17睡眠和唤醒睡眠和唤醒一旦一个等待队列被声明和初始化之后,进程就可以使用这个等待队列进入睡眠。如下函数用于使一个进程进入睡眠和唤醒一个睡眠中的进程:sleep_on(wait_queue_head_t*queue);interruptible_sleep_on(wait_queue_head_t*queue);sleep_on_timeout(wait_queue_head_t*queue,long timeout);interruptible_sleep_on_timeout(wait_queue_head_t*queue,long timeout);sleep_on(wait_queue_head_t*queue);interruptible_sleep_on(wait_queue_head_t*queue);void wait_event(wait_queue_head_t queue,int condition);int wait_event_interruptible(wait_queue_head_t queue,int condition);wake_up(wait_queue_head_t*queue);wake_up_interruptible(wait_queue_head_t*queue);wake_up_sync(wait_queue_head_t*queue);wake_up_interruptible_sync(wait_queue_head_t*queue);18时间管理:时间间隔、时间延迟时间管理:时间间隔、时间延迟Linux操作系统的时钟产生固定时间间隔的时钟中断。时间间隔由HZ值决定。HZ:这是一个与体系结构有关的值,在大多数的体系结构上,Linux操作系统定义这个值为100,表示每一秒种的时钟中断次数。jiffies:这个变量是一个操作系统的全局变量。系统启动时这个变量被初始化成0,并在随后的每次时钟中断时被加1。每次时钟中断也被称为一次“滴答(tick)”。另外,目前的大多数CPU都有一个计数寄存器,专门用于记录CPU的时钟脉冲,它每个时钟周期被加1。这个寄存器记录的数据不受任何CPU其它操作的影响,最真实地记录了时间的变化。这个寄存器的值可以通过函数或系统调用在内核空间或用户空间被读出使用,如使用void do_gettimeofday(struct timeval*tv);gettimeofday(struct timeval*tv);19时间延迟时间延迟确定时间间隔的时间延迟:长间隔延迟短间隔延迟任务的延迟执行:任务队列(task queue)tasklet内核定时器(kernel timer)unsigned ling j=jiffies+delay*HZ while(jiffies j)/*do nothing*/;sleep_on_timeout(wait_queue_head_t*q,unsigned long t);interruptible_sleep_on_timeout(.);void udelay(unsigned long usecs);void mdelay(unsigned long msecs);20内存的申请和分配内存的申请和分配驱动程序有时需要动态申请一些内存用于暂存数据等。驱动程序申请的内存空间在内核地址空间范围内,因此它们会被保持在内存中而不会被交换到磁盘的交换空间上。驱动程序用于申请内存空间的函数一般为:void*kmalloc(size_t size,int flags);void kfree(const void*addr);get_free_page(int flags);get_free_pages(int flags,unsigned long order);void free_page(unsigned long addr);void free_pages(unsigned long addr,unsigned long order);size:申请的字节数flags:控制内存分配的标志参数GFP_KERNEL:常用的标志,但可能导致进程睡眠,因此不能用于中断响应函数中。GFP_ATOMIC:不会导致进程睡眠,如果没有内存可用,则内存分配失败。GFP_BUFFER,GFP_USER,GFP_HIGHMEM,_GFP_DMA,_GFP_HIGHMEM按页面申请内存空间。flags与kmalloc中的含义相同,order为以2为底的指数,如order=3表示申请8个页面。void*vmalloc(unsigned long size);void vfree(void*addr);void*ioremap(unsigned long offset,unsigned long size);void iounmap(void*addr);用于在虚拟地址空间中申请内存空间。21使用使用devfs2.4版本Linux提供了对Device Filesystem的支持。使用devfs可以在设备驱动程序初始化时自动在/dev目录中创建相应的设备文件,并在设备驱动程序注销时自动移除相应的设备文件。使用devfs要求内核编译时定义了符号CONFIG_DEVFS_FSdevfs_handle_t devfs_register(devfs_handle_t dir,const char*name,unsigned int flags,unsigned int major,unsigned int minor,umode_t mode,void*ops,void*info);void devfs_unregister(devfs_handle_t deventry);这两个函数用于向文件系统添加和移除设备文件int devfs_register_chrdev(unsigned int major,const char*name,struct file_operations fops);int devfs_unregister_chrdev(unsigned int major,const char*name);这两个函数用于向内核注册和撤消一个字符设备22设备号的动态分配设备号的动态分配虽然每个设备都有一个设备号(实际包括主设备号和次设备号),但是当文件系统建立之后,我们使用的是设备的文件名,并不直接使用设备号。因此从应用的角度看,一个设备被分配哪个设备号并不重要。Linux操作系统提供了设备号的动态分配机制。int result;.result=register_chrdev(spioc_major,“spioc”,&spioc_fops);if(result 0)printk(KERN_WARNING“spioc:cant get major%dn”,spioc_major);return result;if(spioc_major=0)spioc_major=result;.unregister_chrdev(spioc_major,“spioc”);上述方法同样适用于devfs_register_chrdev()函数。23驱动程序的调试驱动程序的调试使用printk():printk(KERN_DEBUG“some messages here with variable%d.n”,value);共有8个记录级别(loglevel):KERN_EMERG,KERN_ALERT,KERN_CRIT,KERN_ERR,KERN_WARNING,KERN_NOTICE,KERN_INFO,KERN_DEBUG使用/proc文件系统:/proc文件系统是一个特殊的由软件建立的文件系统,内核利用它导出信息。/proc下的每个文件都与内核的一个功能/函数相联系,当用户读取相应的文件时,这些功能/函数的信息就被输出了。使用/proc文件系统主要用于查询一些内核信息。cat/proc/modules使用测试程序:利用用户级的应用程序测试驱动程序,对驱动程序的运行进行观察。使用系统出错记录:使用系统出错转储(dump)信息。使用gdb:使用gdb可以对运行中的内核进行有限调试,主要是读取一些内核信息。使用kdb:第三方提供的一个内核调试器(oss.sgi)。使用kdb需要给内核打上相应的补丁,并重新编译和安装内核。目前kdb对不同体系结构的支持非常有限。24安全性安全性安全性是操作系统非常重要的一个方面,也是一个非常复杂的课题。驱动程序因为是作为内核的一部分而工作,所以驱动程序的安全性和内核的安全性同等重要,因而也是整个操作系统的安全性。1.对设备使用的控制:设备文件的使用;只能单次打开的设备;只能由一个用户打开的设备;.2.驱动程序内部数据的使用:对数据边界的检测;内存泄露;缓冲区溢出;内存空间的初始化;.3.驱动程序结构的合理性:中断的处理;竞争条件的避免;对错误的细致处理;.4.驱动程序代码的可重入性(reentrant code):状态变量和私有数据的处理。25实例:在spioc中使用中断和devfs中断源:键盘中断使用方式:数据的读(或写)由中断控制执行tasklet的使用使用devfs26NET-ARM2410开发板键盘电路原理图开发板键盘电路原理图键盘产生的中断,使用S3C2410外部中断 EINT4IIC接口键盘及LED控制器27中断号中断号我们使用的中断源是s3c2410的一个外部中断信号线,EINT4,它的中断号是固定的。在include/asm-arm/arch-s3c2410/目录下的hardware.h和irqs.h中有关于s3c2410的各种寄存器以及各种资源,如地址、中断号等的定义。为了使用这些量,我们要在引用头文件时包含这两个头文件,如下所示:#include#include.#define NORMAL_IRQ_OFFSET32#define IRQ_EINT4(0+NORMAL_IRQ_OFFSET)#define IRQ_EINT5(1+NORMAL_IRQ_OFFSET).在include/asm-arm/arch-s3c2410/irqs.h中有如下定义28中断响应函数中断响应函数我们使用的中断源是s3c2410的一个外部中断信号线,EINT4,它的中断号是固定的。在include/asm-arm/arch-s3c2410/目录下的hardware.h和irqs.h中有关于s3c2410的各种寄存器以及各种资源,如地址、中断号等的定义。为了使用这些量,我们要在引用头文件时包含这两个头文件,如下所示:#include#include 中断响应函数static void spioc_interrupt(int irq,void*dev_id,struct pt_regs*regs)printk(kbd interrupt received.n);/*do something,such read/write data to I/O port*/*left other things to tasklet*/tasklet_schedule(&spioc_tasklet);return;29tasklet初始化taskletstatic void spioc_do_tasklet(unsigned long);DECLARE_TASKLET(spioc_tasklet,spioc_do_tasklet,0);taskletstatic void spioc_do_tasklet(unsigned long)/*do some data process here*/int i;for(i=0;i count)cnt=count;copy_to_user(buf,rbuff,cnt);return cnt;假设“读”操作是中断驱动的,则读函数应该在等待队列上“睡眠”,直到中断的到来。31注册中断响应函数注册中断响应函数如果这个中断信号只由我们这个驱动程序模块单独使用,则我们可以在驱动程序初始化阶段注册中断响应函数。我们以不共享的方式使用这个中断。static int _init spioc_init(void)int result;.result=request_irq(_IRQ_EINT4,spioc_interrupt,SA_INTERRUPT,spioc,NULL);if(result)printk(spioc:irq request failed.n);/*some furthertreatment for this case*/.32初始化中断控制器和打开初始化中断控制器和打开/关闭中断关闭中断中断控制器的初始化应该在操作系统启动阶段的中断初始化阶段完成了。操作系统的移植者已经帮我们完成了这个工作。然而,对某些微控制器,它的管脚可能是复用的,而缺省状态又不是作为中断输入引脚,这时如果打开中断,可能造成系统“死掉”,比如电平响应的中断,将不停地执行中断响应程序,导致系统其它代码没有机会运行。static int spioc_open(.).enable_irq(IRQ_EINT4);.static int spioc_close(.).disable_irq(IRQ_EINT4);.33使用使用devfs注意,是否能使用devfs与内核是否支持这个特性相关。内核在编译时应该设定CONFIG_DEVFS_FS符号。我们还可以使用动态获得的设备号。#include static devfs_handle_t devfs_spioc;static int _init spioc_init(.)int result;.result=devfs_register_chrdev(0,&spioc_fops);if(result 0)return result;spioc_major=result;devfs_spioc=devfs_register(NULL,spioc,DEVFS_FL_DEFAULT,spioc_major,0,S_IFCHR|S_IRUSR|S_IWUSR,&spioc_fops,NULL);.static void _exit spioc_exit(void).devfs_unregister(devfs_spioc);devfs_unregister_chrdev(spioc_major,spioc);.34块设备块设备基本概念:块操作为基础,速度是主要考虑因素,类型复杂。与字符设备相似之处:注册/撤消,设备号,block_device_operations。块设备的基本操作块设备的读写:request。其它特性:如可移动性,分区等。块设备的加载和卸载:文件系统。35面向块数据的操作,数据块的大小主要由经验值来确定,一般为2的整数幂次字节大小,如4kB,16kB等。块设备是用于存储大量数据的设备,主要是各种数据存储介质设备,如硬盘,软盘,光盘,以及U盘等。出于效率的要求,块设备的数据传输几乎都使用较大的缓冲区,并使用请求队列。块设备主要由文件系统使用,因此,块设备上几乎都要建立文件系统,要有磁盘分区。应用几乎不直接使用块设备,而是通过文件系统使用块设备,如各种应用程序从磁盘的文件中读数据和向磁盘文件写入数据。块设备的操作要比字符设备复杂许多,如磁盘电机的启动/停止操作,磁盘坏块的处理等。块设备在在/dev目录下有相应的设备文件,有主设备号和次设备号。每个磁盘有一个主设备号,一个磁盘上得不同分区使用不同的次设备号。速度和效率是块设备要考虑的主要因素,为提高效率,块设备驱动程序一般都实现一定程度的预读功能。由于使用了缓冲区,磁盘中的数据需要经常与系统缓冲的数据保持同步。否则会导致文件系统崩溃。块设备基本概念块设备基本概念36块设备的注册和注销块设备的注册和注销#inclide int register_blkdev(unsigned int major,const char*name,struct block_device_operations*bdops);int unregister blkdev(unsigned int major,const char*name);struct block_device_operations int(*open)(struct inode*inode,struct file*filp);int(*release)(struct inode*inode,struct file*filp);int(*ioctl)(struct inode*inode,struct file*filp,unsigned command,unsigned long arg);int(*check_media_change)(kdev_t dev);int(*revalidate)(kdev_t dev);#include extern void register_disk(struct gendisk*dev,kdev_t first,unsigned minors,struct block_device_operations*ops,long size);37块设备的打开和关闭块设备的打开和关闭用户的程序一般只打开和关闭磁盘上的文件,并不打开块设备本身。例外的情况是使用如fdisk一类的应用程序对磁盘进行分区、使用mount命令加载一个文件系统等。这些程序将执行块设备文件的打开/关闭操作。fdisk/dev/hdamke2fs/dev/hda3mount-t ext2/dev/hda3/my_mount_pointumount/my_mount_point38块设备的读写块设备的读写#inclide blk_init_queue(request_queue_t*queue,request_fn_proc*request);blk_cleanup_queue(request_queue_t*queue);void request(request_queue_t*queue);每当文件系统想想磁盘写入数据或打算从磁盘读出数据时,它就调用驱动程序的rerquest函数,将读写请求放到一个请求队列中去。这是一个需要由驱动程序开发者完成的函数。这个函数的主要结构是一个无限的循环,不停地检查等待队列,处理等待队列中的请求。被放到等待队列中的元素是request struct类型的结构体。block_device_operations中没有read和write这两个函数。实际块设备的读写不是使用read/write完成的。出于效率的考虑,块设备的读写使用了请求队列。3940可移动设备可移动设备块设备驱动程序需要处理可移动介质的情况,如软盘、U盘等。struct block_device_operations int(*open)(struct inode*inode,struct file*filp);int(*release)(struct inode*inode,struct file*filp);int(*ioctl)(struct inode*inode,struct file*filp,unsigned command,unsigned long arg);int(*check_media_change)(kdev_t dev);int(*revalidate)(kdev_t dev);块设备驱动程序提供两个函数用于检测介质改变的情况以及介质改变后重新使之可用:check_media_change(kdev_t dev)revalidate(kdev_t dev)41网络设备网络设备的特性内核中的网络设备驱动模块网络设备的打开和关闭数据包的发送和接收套接口(Socket)及其操作42网络设备特性网络设备特性面向数据包的操作,与块设备有相似之处,系统上的网络接口类似于加载的块设备:块设备注册块设备结构体到内核的块设备列表中,网络设备注册网络设备到内核的网络设备列表中。块设备根据“请求”传送和接收“数据块”,网络设备则与外部世界交换数据包。网络设备在/dev目录下没有相应的设备文件,也没有主设备号、次设备号等。网络设备驱动程序主要由操作系统的网络子系统使用,用户通过使用套接口使用网络设备。网络设备驱动程序不实现网络协议。网络协议由网络子系统实现。网络设备与块设备最大的不同之处是:块设备只接收内核的request完成块数据的读写,但网络设备要异步地接收外部世界发来的数据包43内核中的网络设备模块内核中的网络设备模块每个网络设备在内核中由一个struct net_device数据结构表示。这是一个比较大的结构体,在include/linux/netdevice.h中声明。在net_device结构体中有一个函数指针,它指向设备的初始化函数。当你向内核注册一个网络设备时,内核让驱动程序自己初始化网络设备和驱动程序本身。网络设备保存在在内核中的网络设备列表中。每个网络设备的名字保存在net_device结构体的name域中,对于以太网设备,一般使用eth0,eth1等表示第一个、第二个网络设备。驱动程序通过调用如下的函数注册和撤消一个网络设备:register_netdev(struct net_device*dev);unregister_netdev(struct net_device*dev);44网络设备的打开和关闭网络设备的打开和关闭与块设备类似,网络设备也不是有用户直接使用的,而是由网络子系统来使用的。因此网络设备的打开也和块设备类似,通过使用专用的应用程序来打开。如下命令用于打开和关闭网络设备:ifconfig eth0 upifconfig eth0 downifconfig命令还可以对网络设备的参数进行配置,如分配网卡的IP地址等。45数据包的发送和接收数据包的发送和接收与块设备类似,网络设备也不使用读/写操作来读写数据,而是使用数据包的发送和接收来实现“读写”功能。这里,“读”表示网络设备接收到外部世界发来的数据包,这其实是一个被动的过程。“写”则是网络子系统通过网络设备向外发送数据的过程。数据包的发送是主动和同步的,为了效率也需要使用缓冲区。数据的接收则是需要通过中断来驱动的,因为何时有外部数据完全是不确定的,需要一个中断信号来提醒操作系统。46作业写出完整的spioc设备驱动程序源程序Makefile将设备驱动程序编译成可加载内核模块根据如下要求丰富驱动程序spioc根据搏创的NET-ARM2410实验平台,使用键盘作为中断源,将spioc的驱动程序增加中断响应功能,使得每次有击键动作后驱动程序才响应应用程序的读或写请求。使用devfs特性。编译并调试你的驱动程序。写一个应用程序测试你的驱动程序作业提交内容驱动程序源代码测试程序源代码编译命令或Makefile文件47参考资料Linux设备驱动程序4826、要使整个人生都过得舒适、愉快,这是不可能的,因为人类必须具备一种能应付逆境的态度。卢梭27、只有把抱怨环境的心情,化为上进的力量,才是成功的保证。罗曼罗兰28、知之者不如好之者,好之者不如乐之者。孔子29、勇猛、大胆和坚定的决心能够抵得上武器的精良。达芬奇30、意志是一个强壮的盲人,倚靠在明眼的跛子肩上。叔本华谢谢!谢谢!49
展开阅读全文