博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
《Linux Device Driver》——中断处理
阅读量:4157 次
发布时间:2019-05-26

本文共 14535 字,大约阅读时间需要 48 分钟。

尽管有些设备仅仅通过控制其寄存器就可以得到控制,但现实中的大部分设备却要比这复杂一些。因为大部分设备的处理时间与处理器不在同一个周期,且一定会比处理器慢的多,这就造成了一种让处理器等待设备的现象,显然这是不行的,而有一种解决方法就是中断操作。

中断仅仅就是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。 Linux 处理中断的方式非常类似在用户空间处理信号的方式。大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。本质上来讲,中断处理例程和其他的代码并行运行。因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。 透彻地理解并发控制技术对中断来讲非常重要。

安装中断处理例程

内核维护了一个中断信号线的注册表,改注册表类似于I/O端口的注册表。模块在使用中断之前要先请求一个中断通道(或者中断请求IRQ),然后在使用后释放该通道。

int request_irq(unsigned long irq, irqreturn_t (*handler)(int,void *,struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id);/*表示注册中断,返回值: 0 指示成功,或返回一个负的错误码,如 -EBUSY 表示另一个驱动已经占用了你所请求的中断线。*//* 参数:unsigned int irq:要申请的中断号irqreturn_t (*handler)(int,void *,struct pt_regs *):要安装的中断处理函数的指针unsigned long flags:与中断管理相关的位掩码选项const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts显示中断的拥有者void *dev_id:用于共享的中断信号线,它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。*//* flags:SA_INTERRUPT :快速中断标志。快速中断处理例程运行在当前处理器禁止中断的状态下。SA_SHIRQ : 在设备间共享中断标志。SA_SAMPLE_RANDOM :该位表示产生的中断能对 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有贡献。 读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。 若设备以真正随机的周期产生中断,就应当设置这个标志。若设备中断是可预测的,这个标志不值得设置。可能被攻击者影响的设备不应当设置这个标志。更多信息 看 drivers/char/random.c 的注释。 */

中断处理例程可在驱动初始化时或在设备第一次打开时安装。推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。

void free_irq(unsigned int irq,void *dev_id); //释放中断

这样调用 free_irq 的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。但这种方式的缺点是必须为每个设备维护一个打开计数。

下面的中断申请的示例(并口):

if (short_irq >= 0){
result = request_irq(short_irq, short_interrupt, SA_INTERRUPT, "short", NULL); if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n", short_irq); short_irq = -1; } else {
/*打开中断硬件的中断能力*/ outb(0x10,short_base+2); }}

i386 和 x86_64 体系定义了一个函数来查询一个中断线是否可用:

int can_request_irq(unsigned int irq, unsigned long flags);/*当能够成功分配给定中断,则返回非零值。但注意,在 can_request_irq 和 request_irq 的调用之间给定中断可能被占用*/

/proc接口

当硬件中断到达处理器时, 内核提供的一个内部计数器会递增,产生的中断报告显示在文件 /proc/interrupts中。这一方法可以用来检查设备是否按预期地工作。此文件只显示当前已安装处理例程的中断的计数。若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。

/proc/stat记录了几个关于系统活动的底层统计信息, 包括(但不仅限于)自系统启动以来收到的中断数。stat 的每一行以一个字符串开始, 是该行的关键词:intr 标志是中断计数。第一个数是所有中断的总数, 而其他每一个代表一个单独的中断线的计数, 从中断 0 开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。

以上两个文件的一个不同是:/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在<asm/irq.h>中。

ARM的定义为:

#define NR_IRQS    128

自动检测IRQ号

驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:

if (short_irq < 0) /* 依靠使并口的端口号,确定中断*/switch(short_base) {
case 0x378: short_irq = 7; break;case 0x278: short_irq = 2; break;case 0x3bc: short_irq = 5; break;}

这段代码根据选定的I/O地址的基地址分配中断号,也允许用户在装载时用下面的命令来覆盖默认值:

insmod xxxxx.ko irq=x

当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。

但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探测在理论上简单,但实现可能不简单。

有 2 种方法来进行探测中断:调用内核定义的辅助函数和DIY探测。


内核帮助下的检测

linux提供了一个底层设施来探测中断号。它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并可提供更好的找到配置中断号的方法,内核提供的这一设施由两个函数组成,在头文件<linux/interrupt.h>中声明:

unsigned long probe_irq_on(void);/*返回一个未分配中断的位掩码。驱动必须保留返回的位掩码,并在后边传递给probe_irq_off,在调用它之后,驱动程序应当至少安排它的设备产生一次中断*/int probe_irq_off(unsigned long);/*在请求设备产生一次中断后,驱动调用这个函数,并将probe_irq_on产生的位掩码作为参数传递给 probe_irq_off,probe_irq_off返回在probe_on之后发生的中断号。如果没有中断发生,返回0,如果产生了多次中断,返回一个负值。*/

程序员应当注意在调用 probe_irq_on 之后启用设备上的中断, 并在调用 probe_irq_off 前禁用。此外还必须记住在 probe_irq_off 之后服务设备中待处理的中断。

short模块演示了如何进行这样的探测(指定probe=1选项装载模块,并口的管脚 9 和 10 连接在一起,探测五次失败后放弃):

int count= 0;do{
unsigned long mask; mask = probe_irq_on(); outb_p(0x10,short_base+2);/* enable reporting */ outb_p(0x00,short_base);/* clear the bit */ outb_p(0xFF,short_base);/* set the bit: interrupt! */ outb_p(0x00,short_base+2);/* disable reporting */ udelay(5);/* give it some time */ short_irq = probe_irq_off(mask); if (short_irq== 0){
/* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); short_irq = -1; }} while (short_irq < 0 && count++< 5);if (short_irq< 0) printk("short: probe failed %i times, giving up\n",count);

最好只在模块初始化时探测中断线一次。

大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。


DIY探测

如果装载时probe=2,short模块将对IRQ信号线进行DIY探测。

DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。

下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 trials 数组列出要尝试的中断, 以 0 作为结尾标志; tried 数组用来跟踪哪个中断号已经被这个驱动注册。

int trials[]= {
3, 5, 7, 9, 0};int tried[]={
0, 0, 0, 0, 0};int i, count = 0;for (i = 0; trials[i]; i++) tried[i]= request_irq(trials[i], short_probing, SA_INTERRUPT, "short probe", NULL);do{
short_irq = 0;/* none got, yet */ outb_p(0x10,short_base+2);/* enable */ outb_p(0x00,short_base); outb_p(0xFF,short_base);/* toggle the bit */ outb_p(0x00,short_base+2);/* disable */ udelay(5);/* give it some time */ /* 等待中断,若在这段时间有中断产生,handler会改变 short_irq */ /* the value has been set by the handler */ if (short_irq== 0){
/* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); } } while (short_irq <=0&&count++< 5);/* end of loop, uninstall the handler */for (i = 0; trials[i]; i++) if (tried[i]== 0) free_irq(trials[i],NULL);if (short_irq< 0) printk("short: probe failed %i times, giving up\n",count);

以下是handler的源码:

irqreturn_t short_probing(int irq,void*dev_id,struct pt_regs*regs){
if (short_irq== 0) short_irq= irq;/* found */ if (short_irq!= irq) short_irq=-irq;/* ambiguous */ return IRQ_HANDLED;}

若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1


快速和慢速处理例程

快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。在现代内核中,快速和慢速中断的区别已经消失,剩下的只有一个:快速中断(使用 SA_INTERRUPT )执行时禁止所有在当前处理器上的其他中断。注意:其他的处理器仍然能够处理中断。

除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.


x86中断处理内幕

这个描述是从 2.6 内核 arch/i386/kernel/irq.c, arch/i386/kernel/ apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, 和 include/asm-i386/hw_irq.h 中得出,尽管基本概念相同,硬件细节与其他平台上不同。

底层中断处理代码在汇编语言文件 entry.S。在所有情况下,这个代码将中断号压栈并且跳转到一个公共段,公共段会调用 do_IRQ(在 irq.c 中定义)。do_IRQ 做的第一件事是应答中断以便中断控制器能够继续其他事情。它接着获取给定 IRQ 号的一个自旋锁,阻止其他 CPU 处理这个 IRQ,然后清除几个状态位(包括IRQ_WAITING )然后查找这个 IRQ 的处理例程。若没有找到,什么也不做;释放自旋锁,处理任何待处理的软件中断,最后 do_IRQ 返回。从中断中返回的最后一件事可能是一次处理器的重新调度。

IRQ的探测是通过为每个缺乏处理例程的IRQ设置 IRQ_WAITING 状态位来完成。当中断发生, 因为没有注册处理例程,do_IRQ 清除这个位并且接着返回。 当probe_irq_off被一个函数调用,只需搜索没有设置 IRQ_WAITING 的 IRQ。


实现中断处理例程

1.中断处理例程是在中断时间内运行的,因此行为会受到一些限制。这些限制跟我们在内核定时器中看到的一样。

处理例程不能向用户空间发送或者接收数据,因为它不是在任何进程上下文中执行的    处理例程也不能做任何可能发生休眠的操作,例如调用wait_event,使用不带GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量等等    处理例程不能调用schdule函数

2.中断处理例程的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的终端的不同含义对数据进行相应的读或写。中断处理例程第一步常常包括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样的设备是少数。

3.中断处理的一个典型任务:如果中断通知进程所等待的事件已经发生,比如新的数据已经到达,就会唤醒在设备上休眠的进程。
不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务。

在short示例代码中,中断处理例程调用了do_gettimeofday,并输出当前时间到大小为一页的循环缓冲区,然后唤醒任何一个读取进程,告诉该进程现在又新的数据可以读取。

irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs){
struct timeval tv; int written; do_gettimeofday(&tv); /* Write a 16 byte record. Assume PAGE_SIZE is a multiple of 16 */ written = sprintf((char *)short_head,"%08u.%06u\n", (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec)); BUG_ON(written != 16); short_incr_bp(&short_head, written); wake_up_interruptible(&short_queue); /* awake any reading process */ return IRQ_HANDLED;}

上述代码代表了中断处理例程的典型工作流程。它所调用的short_incr_bp函数定义如下:

static inline void short_incr_bp(volatile unsigned long *index,int delta){
unsigned long new=*index + delta; barrier(); /*ban optimize*/ *index = (new >= (short_buffer + PGE_SIZE)) ? short_buffer : new;}

这个函数的实现非常谨慎,它可以将指针限制在循环缓冲区的范围之内,并且不会因传递一个不正确的值而返回。对 barrier的调用将阻止编译器在函数的两行语句之间数何优化工作。如果没有这个屏障,编译器可能会优化出一个新的变量并将其直接赋值index。在 index发生反转的短暂时间内,这种优化可能会产生不正确的索引值。通仔细处理其他线程可见的值来避免出现不一致的情况,我们就可以不使用锁而安全操作循环缓冲区的指针。

下面代码实现了对/dev/shortint的读取和写入:

ssize_t short_i_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
int count0; DEFINE_WAIT(wait); while (short_head == short_tail) {
prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE); if (short_head == short_tail) schedule(); finish_wait(&short_queue, &wait); if (signal_pending (current)) /* a signal arrived */ return -ERESTARTSYS; /* tell the fs layer to handle it */ } /* count0 is the number of readable data bytes */ count0 = short_head - short_tail; if (count0 < 0) /* wrapped */ count0 = short_buffer + PAGE_SIZE - short_tail; if (count0 < count) count = count0; if (copy_to_user(buf, (char *)short_tail, count)) return -EFAULT; short_incr_bp (&short_tail, count); return count;}ssize_t short_i_write (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
int written = 0, odd = *f_pos & 1; unsigned long port = short_base; /* output to the parallel data latch */ void *address = (void *) short_base; if (use_mem) {
while (written < count) iowrite8(0xff * ((++written + odd) & 1), address); } else {
while (written < count) outb(0xff * ((++written + odd) & 1), port); } *f_pos += count; return written;}

启用和禁止中断

有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。注意:应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。

禁止单个中断

有时(但是很少!)一个驱动需要禁止一个特定中断。但不推荐这样做,特别是不能禁止共享中断(在现代系统中, 共享的中断是很常见的)。内核提供了 3 个函数,是内核 API 的一部分,声明在 <asm/irq.h>:

void disable_irq(int irq);/*禁止给定的中断, 并等待当前的中断处理例程结束。如果调用 disable_irq 的线程持有任何中断处理例程需要的资源(例如自旋锁), 系统可能死锁*/void disable_irq_nosync(int irq);/*禁止给定的中断后立刻返回(可能引入竞态)*/void enable_irq(int irq);

调用任一函数可能更新在可编程控制器(PIC)中的特定 irq 的掩码, 从而禁止或使能所有处理器特定的 IRQ。这些函数的调用能够嵌套,即如果 disable_irq 被连续调用 2 次,则需要 2 个 enable_irq 重新使能 IRQ 。可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。

禁用所有的中断

在 2.6 内核, 可使用下面 2 个函数中的任一个(定义在 <asm/system.h>)关闭当前处理器上所有中断:

void local_irq_save(unsignedlong flags);/*在保存当前中断状态到 flags 之后禁止中断*/void local_irq_disable(void);/* 关闭中断而不保存状态*//*如果调用链中有多个函数可能需要禁止中断, 应使用 local_irq_save*//*打开中断使用:*/void local_irq_restore(unsignedlong flags); void local_irq_enable(void);

在2.6内核,没有方法全局禁用整个系统上的所有中断。


顶半部和底半部

中断处理的一个主要问题就是中断处理例程中完成耗时的任务。相应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使中断阻塞的时间过长。这是两个矛盾的需求。

linux通过将中断处理分成两部分来完成这个任务:

1.顶半部:实际响应中断的例程(request_irq注册的那个例程)

2.底半部:被顶半部调用并在稍后更安全的一个时间里执行的函数。

他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。典型的情况是:顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出:这个操作非常快。底半部接着进行任何其他需要的工作。这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。

Linux 内核有 2 个不同的机制可用来实现底半部处理:

1.tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;
2.工作队列,它可能有更高的延时,但允许休眠。


tasklet

tasklet是一个可以在由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。它们可以被多次调度运行,但 tasklet的调度并不会累积;也就是说,实际只会运行一次,即使在激活 tasklet的运行之前重复请求该 tasklet的运行也是这样。不会有同一tasklet的多个实例并行地运行,因为它们只运行一次,但是 tasklet可以与其他的 tasklet并行地运行在对称多处理器(SMP)系统上。这样,如果驱动程序有多个 tasklet,它们必须使用某种锁机制来避免彼此间的冲突。

tasklet可确保和第一次调度它们的函数运行在同样的CPU上。这样,因为 tasklet在中断处理例程结束前并不会开始运行,所以此时的中断处理例程是安全的。不管怎样,在tasklet运行时,当然可以有其他的中断发生,因此在 tasklet和中断处理例程之间的锁还是需要的。
必须使用宏 DECLARE_TASKLE声明 tasklet:

#define DECLARE_TASKLET(name, func, data) \struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }/*name是给tasklet起的名字,func是执行tasklet时调用的函数(它带有一个unsigned long型的参数并且返回void),data是一个用来传递给tasklet函数的unsigned long类型的值。*/

驱动程序short如下声明自己的tasklet:

/* Set up our tasklet if we're doing that. */void short_do_tasklet(unsigned long);DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);

函数tasklet_schedule用来调度一个tasklet运行。如果指定tasklet=1选项装载short,就会安装一个不同的中断处理例程,这个处理例程保存数据并如下调度tasklet:

irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs){
do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning */ short_incr_tv(&tv_head); tasklet_schedule(&short_tasklet); short_wq_count++; /* record that an interrupt arrived */ return IRQ_HANDLED;}

实际的tasklet例程,即short_do_tasklet,将会在系统方便时得到触发。

void short_do_tasklet (unsigned long unused){
int savecount = short_wq_count, written; short_wq_count = 0; /* we have already been removed from the queue */ /* * The bottom half reads the tv array, filled by the top half, * and prints it to the circular text buffer, which is then consumed * by reading processes */ /* First write the number of interrupts that occurred before this bh */ written = sprintf((char *)short_head,"bh after %6i\n",savecount); short_incr_bp(&short_head, written); /* * Then, write the time values. Write exactly 16 bytes at a time, * so it aligns with PAGE_SIZE */ do {
written = sprintf((char *)short_head,"%08u.%06u\n", (int)(tv_tail->tv_sec % 100000000), (int)(tv_tail->tv_usec)); short_incr_bp(&short_head, written); short_incr_tv(&tv_tail); } while (tv_tail != tv_head); wake_up_interruptible(&short_queue); /* awake any reading process */}

工作队列

工作队列会在将来的某个时间、在某个特殊的工作者进程上下文调用一个函数。因为工作队列函数运行在进程上下文中,因此可在必要时休眠。

工作者进程无法访问其他任何进程的地址空间。


中断共享

Linux内核支持在所有总线上中断共享。

安装共享的处理例程

通过 request_irq来安装共享中断与非共享中断有2点不同:

1.当request_irq时,flags中必须指定SA_SHIRQ位;
2.dev_id必须唯一。任何指向模块地址空间的指针都行,但dev_id绝不能设置为NULL。

内核为每个中断维护一个中断共享处理例程列表,dev_id就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。 dev_id用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须是唯一的.

请求一个共享的中断时,如果满足下列条件之一,则request_irq成功:

1.中断线空闲;
2.所有已经注册该中断信号线的处理例程也标识了IRQ是共享。

一个共享的处理例程必须能够识别自己的中断,并且在自己的设备没有被中断时快速退出(返回IRQ_NONE)。

共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。

一个使用共享处理例程的驱动需要小心:不能使用enable_irq或disable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:他的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"。


中断驱动的I/O

当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。

为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:

输入:当新数据到达时并处理器准备好接受时,设备中断处理器。

输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。

转载地址:http://eezxi.baihongyu.com/

你可能感兴趣的文章
laravel事务
查看>>
【JavaScript 教程】浏览器—History 对象
查看>>
这才是学习Vite2的正确姿势!
查看>>
7 个适用于所有前端开发人员的很棒API,你需要了解一下
查看>>
20种在学习编程的同时也可以在线赚钱的方法
查看>>
隐藏搜索框:CSS 动画正反向序列
查看>>
【视频教程】Javascript ES6 教程27—ES6 构建一个Promise
查看>>
【5分钟代码练习】01—导航栏鼠标悬停效果的实现
查看>>
127个超级实用的JavaScript 代码片段,你千万要收藏好(中)
查看>>
127个超级实用的JavaScript 代码片段,你千万要收藏好(下)
查看>>
Flex 布局教程:语法篇
查看>>
年薪50万+的90后程序员都经历了什么?
查看>>
2019年哪些外快收入可达到2万以上?
查看>>
【JavaScript 教程】标准库—Date 对象
查看>>
前阿里手淘前端负责人@winter:前端人如何保持竞争力?
查看>>
【JavaScript 教程】面向对象编程——实例对象与 new 命令
查看>>
我在网易做了6年前端,想给求职者4条建议
查看>>
SQL1015N The database is in an inconsistent state. SQLSTATE=55025
查看>>
RQP-DEF-0177
查看>>
MySQL字段类型的选择与MySQL的查询效率
查看>>