内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同的:抛开漫不经心,你必须小心翼翼,因为你编程中的一个bug就会影响到整个系统,本文给大家介绍linux内核模块编写,需要的朋友可以参考下

内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中,它八成就是了。Linux内核和它的用户空间是大不相同的:抛开漫不经心,你必须小心翼翼,因为你编程中的一个bug就会影响到整个系统。浮点运算做起来可不容易,堆栈固定而狭小,而你写的代码总是异步的,因此你需要想想并发会导致什么。而除了所有这一切之外,Linux内核只是一个很大的、很复杂的C程序,它对每个人开放,任何人都去读它、学习它并改进它,而你也可以是其中之一。

学习内核编程的最简单的方式也许就是写个内核模块:一段可以动态加载进内核的代码。模块所能做的事是有限的——例如,他们不能在类似进程描述符这样的公共数据结构中增减字段(LCTT译注:可能会破坏整个内核及系统的功能)。但是,在其它方面,他们是成熟的内核级的代码,可以在需要时随时编译进内核(这样就可以摒弃所有的限制了)。完全可以在Linux源代码树以外来开发并编译一个模块(这并不奇怪,它称为树外开发),如果你只是想稍微玩玩,而并不想提交修改以包含到主线内核中去,这样的方式是很方便的。

在本教程中,我们将开发一个简单的内核模块用以创建一个/dev/reverse设备。写入该设备的字符串将以相反字序的方式读回(“Hello World”读成“World Hello”)。这是一个很受欢迎的程序员面试难题,当你利用自己的能力在内核级别实现这个功能时,可以使你得到一些加分。在开始前,有一句忠告:你的模块中的一个bug就会导致系统崩溃(虽然可能性不大,但还是有可能的)和数据丢失。在开始前,请确保你已经将重要数据备份,或者,采用一种更好的方式,在虚拟机中进行试验。

尽可能不要用root身份

默认情况下,/dev/reverse只有root可以使用,因此你只能使用sudo来运行你的测试程序。要解决该限制,可以创建一个包含以下内容的/lib/udev/rules.d/99-reverse.rules文件:

SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
别忘了重新插入模块。让非root用户访问设备节点往往不是一个好主意,但是在开发其间却是十分有用的。这并不是说以root身份运行二进制测试文件也不是个好主意。
模块的构造

由于大多数的Linux内核模块是用C写的(除了底层的特定于体系结构的部分),所以推荐你将你的模块以单一文件形式保存(例如,reverse.c)。我们已经把完整的源代码放在GitHub上——这里我们将看其中的一些片段。开始时,我们先要包含一些常见的文件头,并用预定义的宏来描述模块:

这里一切都直接明了,除了MODULE_LICENSE():它不仅仅是一个标记。内核坚定地支持GPL兼容代码,因此如果你把许可证设置为其它非GPL兼容的(如,“Proprietary”[专利]),某些特定的内核功能将在你的模块中不可用。

bash/shell Code复制内容到剪贴板

什么时候不该写内核模块

内核编程很有趣,但是在现实项目中写(尤其是调试)内核代码要求特定的技巧。通常来讲,在没有其它方式可以解决你的问题时,你才应该在内核级别解决它。以下情形中,可能你在用户空间中解决它更好:

你要开发一个USB驱动 —— 请查看libusb。
你要开发一个文件系统 —— 试试FUSE。
你在扩展Netfilter —— 那么libnetfilter_queue对你有所帮助。
通常,内核里面代码的性能会更好,但是对于许多项目而言,这点性能丢失并不严重。
由于内核编程总是异步的,没有一个main()函数来让Linux顺序执行你的模块。取而代之的是,你要为各种事件提供回调函数,像这个:

bash/shell Code复制内容到剪贴板

这里,我们定义的函数被称为模块的插入和删除。只有第一个的插入函数是必要的。目前,它们只是打印消息到内核环缓冲区(可以在用户空间通过dmesg命令访问);KERN_INFO是日志级别(注意,没有逗号)。__init和__exit是属性 —— 联结到函数(或者变量)的元数据片。属性在用户空间的C代码中是很罕见的,但是内核中却很普遍。所有标记为__init的,会在初始化后释放内存以供重用(还记得那条过去内核的那条“Freeing unused kernel memory…[释放未使用的内核内存……]”信息吗?)。__exit表明,当代码被静态构建进内核时,该函数可以安全地优化了,不需要清理收尾。最后,module_init()和module_exit()这两个宏将reverse_init()和reverse_exit()函数设置成为我们模块的生命周期回调函数。实际的函数名称并不重要,你可以称它们为init()和exit(),或者start()和stop(),你想叫什么就叫什么吧。他们都是静态声明,你在外部模块是看不到的。事实上,内核中的任何函数都是不可见的,除非明确地被导出。然而,在内核程序员中,给你的函数加上模块名前缀是约定俗成的。

这些都是些基本概念 – 让我们来做更多有趣的事情吧。模块可以接收参数,就像这样:

# modprobe foo bar=1

modinfo命令显示了模块接受的所有参数,而这些也可以在/sys/module//parameters下作为文件使用。我们的模块需要一个缓冲区来存储参数 —— 让我们把这大小设置为用户可配置。在MODULE_DESCRIPTION()下添加如下三行:

bash/shell Code复制内容到剪贴板

这儿,我们定义了一个变量来存储该值,封装成一个参数,并通过sysfs来让所有人可读。这个参数的描述(最后一行)出现在modinfo的输出中。

由于用户可以直接设置buffer_size,我们需要在reverse_init()来清除无效取值。你总该检查来自内核之外的数据 —— 如果你不这么做,你就是将自己置身于内核异常或安全漏洞之中。

bash/shell Code复制内容到剪贴板

来自模块初始化函数的非0返回值意味着模块执行失败。

导航

但你开发模块时,Linux内核就是你所需一切的源头。然而,它相当大,你可能在查找你所要的内容时会有困难。幸运的是,在庞大的代码库面前,有许多工具使这个过程变得简单。首先,是Cscope —— 在终端中运行的一个比较经典的工具。你所要做的,就是在内核源代码的顶级目录中运行make cscope && cscope。Cscope和Vim以及Emacs整合得很好,因此你可以在你最喜爱的编辑器中使用它。

如果基于终端的工具不是你的最爱,那么就访问http://lxr.free-electrons.com吧。它是一个基于web的内核导航工具,即使它的功能没有Cscope来得多(例如,你不能方便地找到函数的用法),但它仍然提供了足够多的快速查询功能。
现在是时候来编译模块了。你需要你正在运行的内核版本头文件(linux-headers,或者等同的软件包)和build-essential(或者类似的包)。接下来,该创建一个标准的Makefile模板:

bash/shell Code复制内容到剪贴板

现在,调用make来构建你的第一个模块。如果你输入的都正确,在当前目录内会找到reverse.ko文件。使用sudo insmod reverse.ko插入内核模块,然后运行如下命令:

bash/shell Code复制内容到剪贴板

恭喜了!然而,目前这一行还只是假象而已 —— 还没有设备节点呢。让我们来搞定它。

混杂设备

在Linux中,有一种特殊的字符设备类型,叫做“混杂设备”(或者简称为“misc”)。它是专为单一接入点的小型设备驱动而设计的,而这正是我们所需要的。所有混杂设备共享同一个主设备号(10),因此一个驱动(drivers/char/misc.c)就可以查看它们所有设备了,而这些设备用次设备号来区分。从其他意义来说,它们只是普通字符设备。

要为该设备注册一个次设备号(以及一个接入点),你需要声明struct misc_device,填上所有字段(注意语法),然后使用指向该结构的指针作为参数来调用misc_register()。为此,你也需要包含linux/miscdevice.h头文件:

bash/shell Code复制内容到剪贴板

这儿,我们为名为“reverse”的设备请求一个第一个可用的(动态的)次设备号;省略号表明我们之前已经见过的省略的代码。别忘了在模块卸下后注销掉该设备。

bash/shell Code复制内容到剪贴板

‘fops’字段存储了一个指针,指向一个file_operations结构(在Linux/fs.h中声明),而这正是我们模块的接入点。reverse_fops定义如下:

bash/shell Code复制内容到剪贴板

另外,reverse_fops包含了一系列回调函数(也称之为方法),当用户空间代码打开一个设备,读写或者关闭文件描述符时,就会执行。如果你要忽略这些回调,可以指定一个明确的回调函数来替代。这就是为什么我们将llseek设置为noop_llseek(),(顾名思义)它什么都不干。这个默认实现改变了一个文件指针,而且我们现在并不需要我们的设备可以寻址(这是今天留给你们的家庭作业)。

关闭和打开

让我们来实现该方法。我们将给每个打开的文件描述符分配一个新的缓冲区,并在它关闭时释放。这实际上并不安全:如果一个用户空间应用程序泄漏了描述符(也许是故意的),它就会霸占RAM,并导致系统不可用。在现实世界中,你总得考虑到这些可能性。但在本教程中,这种方法不要紧。

我们需要一个结构函数来描述缓冲区。内核提供了许多常规的数据结构:链接列表(双联的),哈希表,树等等之类。不过,缓冲区常常从头设计。我们将调用我们的“struct buffer”:

bash/shell Code复制内容到剪贴板

data是该缓冲区存储的一个指向字符串的指针,而end指向字符串结尾后的第一个字节。read_ptr是read()开始读取数据的地方。缓冲区的size是为了保证完整性而存储的 —— 目前,我们还没有使用该区域。你不能假设使用你结构体的用户会正确地初始化所有这些东西,所以最好在函数中封装缓冲区的分配和收回。它们通常命名为buffer_alloc()和buffer_free()。

bash/shell Code复制内容到剪贴板

内核内存使用kmalloc()来分配,并使用kfree()来释放;kzalloc()的风格是将内存设置为全零。不同于标准的malloc(),它的内核对应部分收到的标志指定了第二个参数中请求的内存类型。这里,GFP_KERNEL是说我们需要一个普通的内核内存(不是在DMA或高内存区中)以及如果需要的话函数可以睡眠(重新调度进程)。sizeof(*buf)是一种常见的方式,它用来获取可通过指针访问的结构体的大小。

你应该随时检查kmalloc()的返回值:访问NULL指针将导致内核异常。同时也需要注意unlikely()宏的使用。它(及其相对宏likely())被广泛用于内核中,用于表明条件几乎总是真的(或假的)。它不会影响到控制流程,但是能帮助现代处理器通过分支预测技术来提升性能。

最后,注意goto语句。它们常常为认为是邪恶的,但是,Linux内核(以及一些其它系统软件)采用它们来实施集中式的函数退出。这样的结果是减少嵌套深度,使代码更具可读性,而且非常像更高级语言中的try-catch区块。

有了buffer_alloc()和buffer_free(),open和close方法就变得很简单了。

bash/shell Code复制内容到剪贴板

struct file是一个标准的内核数据结构,用以存储打开的文件的信息,如当前文件位置(file->f_pos)、标志(file->f_flags),或者打开模式(file->f_mode)等。另外一个字段file->privatedata用于关联文件到一些专有数据,它的类型是void *,而且它在文件拥有者以外,对内核不透明。我们将一个缓冲区存储在那里。

如果缓冲区分配失败,我们通过返回否定值(-ENOMEM)来为调用的用户空间代码标明。一个C库中调用的open(2)系统调用(如glibc)将会检测这个并适当地设置errno 。

学习如何读和写

“read”和“write”方法是真正完成工作的地方。当数据写入到缓冲区时,我们放弃之前的内容和反向地存储该字段,不需要任何临时存储。read方法仅仅是从内核缓冲区复制数据到用户空间。但是如果缓冲区还没有数据,revers_eread()会做什么呢?在用户空间中,read()调用会在有可用数据前阻塞它。在内核中,你就必须等待。幸运的是,有一项机制用于处理这种情况,就是‘wait queues’。

想法很简单。如果当前进程需要等待某个事件,它的描述符(struct task_struct存储‘current’信息)被放进非可运行(睡眠中)状态,并添加到一个队列中。然后schedule()就被调用来选择另一个进程运行。生成事件的代码通过使用队列将等待进程放回TASK_RUNNING状态来唤醒它们。调度程序将在以后在某个地方选择它们之一。Linux有多种非可运行状态,最值得注意的是TASK_INTERRUPTIBLE(一个可以通过信号中断的睡眠)和TASK_KILLABLE(一个可被杀死的睡眠中的进程)。所有这些都应该正确处理,并等待队列为你做这些事。

一个用以存储读取等待队列头的天然场所就是结构缓冲区,所以从为它添加wait_queue_headt read\queue字段开始。你也应该包含linux/sched.h头文件。可以使用DECLARE_WAITQUEUE()宏来静态声明一个等待队列。在我们的情况下,需要动态初始化,因此添加下面这行到buffer_alloc():

bash/shell Code复制内容到剪贴板

我们等待可用数据;或者等待read_ptr != end条件成立。我们也想要让等待操作可以被中断(如,通过Ctrl+C)。因此,“read”方法应该像这样开始:

bash/shell Code复制内容到剪贴板

...

我们让它循环,直到有可用数据,如果没有则使用wait_event_interruptible()(它是一个宏,不是函数,这就是为什么要通过值的方式给队列传递)来等待。好吧,如果wait_event_interruptible()被中断,它返回一个非0值,这个值代表-ERESTARTSYS。这段代码意味着系统调用应该重新启动。file->f_flags检查以非阻塞模式打开的文件数:如果没有数据,返回-EAGAIN。

我们不能使用if()来替代while(),因为可能有许多进程正等待数据。当write方法唤醒它们时,调度程序以不可预知的方式选择一个来运行,因此,在这段代码有机会执行的时候,缓冲区可能再次空出。现在,我们需要将数据从buf->data 复制到用户空间。copy_to_user()内核函数就干了此事:

bash/shell Code复制内容到剪贴板

如果用户空间指针错误,那么调用可能会失败;如果发生了此事,我们就返回-EFAULT。记住,不要相信任何来自内核外的事物!

bash/shell Code复制内容到剪贴板

为了使数据在任意块可读,需要进行简单运算。该方法返回读入的字节数,或者一个错误代码。

写方法更简短。首先,我们检查缓冲区是否有足够的空间,然后我们使用copy_from_userspace()函数来获取数据。再然后read_ptr和结束指针会被重置,并且反转存储缓冲区内容:

bash/shell Code复制内容到剪贴板

这里, reverse_phrase()干了所有吃力的工作。它依赖于reverse_word()函数,该函数相当简短并且标记为内联。这是另外一个常见的优化;但是,你不能过度使用。因为过多的内联会导致内核映像徒然增大。

最后,我们需要唤醒read_queue中等待数据的进程,就跟先前讲过的那样。wake_up_interruptible()就是用来干此事的:

bash/shell Code复制内容到剪贴板

耶!你现在已经有了一个内核模块,它至少已经编译成功了。现在,是时候来测试了。

调试内核代码

或许,内核中最常见的调试方法就是打印。如果你愿意,你可以使用普通的printk() (假定使用KERN_DEBUG日志等级)。然而,那儿还有更好的办法。如果你正在写一个设备驱动,这个设备驱动有它自己的“struct device”,可以使用pr_debug()或者dev_dbg():它们支持动态调试(dyndbg)特性,并可以根据需要启用或者禁用(请查阅Documentation/dynamic-debug-howto.txt)。对于单纯的开发消息,使用pr_devel(),除非设置了DEBUG,否则什么都不会做。要为我们的模块启用DEBUG,请添加以下行到Makefile中:

bash/shell Code复制内容到剪贴板

完了之后,使用dmesg来查看pr_debug()或pr_devel()生成的调试信息。 或者,你可以直接发送调试信息到控制台。要想这么干,你可以设置console_loglevel内核变量为8或者更大的值(echo 8 /proc/sys/kernel/printk),或者在高日志等级,如KERN_ERR,来临时打印要查询的调试信息。很自然,在发布代码前,你应该移除这样的调试声明。

注意内核消息出现在控制台,不要在Xterm这样的终端模拟器窗口中去查看;这也是在内核开发时,建议你不在X环境下进行的原因。惊喜,惊喜!

编译模块,然后加载进内核:

bash/shell Code复制内容到剪贴板

一切似乎就位。现在,要测试模块是否正常工作,我们将写一段小程序来翻转它的第一个命令行参数。main()(再三检查错误)可能看上去像这样:

bash/shell Code复制内容到剪贴板

像这样运行:

bash/shell Code复制内容到剪贴板

现在,让我们让事情变得更好玩一点。我们将创建两个进程,它们共享一个文件描述符(及其内核缓冲区)。其中一个会持续写入字符串到设备,而另一个将读取这些字符串。在下例中,我们使用了fork(2)系统调用,而pthreads也很好用。我也省略打开和关闭设备的代码,并在此检查代码错误(又来了):

bash/shell Code复制内容到剪贴板

你希望这个程序会输出什么呢?下面就是在我的笔记本上得到的东西:

Read: dog lazy the over jumped fox brown quick A
Read: A kcicq brown fox jumped over the lazy dog
Read: A kciuq nworb xor jumped fox brown quick A
Read: A kciuq nworb xor jumped fox brown quick A
...
这里发生了什么呢?就像举行了一场比赛。我们认为read和write是原子操作,或者从头到尾一次执行一个指令。然而,内核确实无序并发的,随便就重新调度了reverse_phrase()函数内部某个地方运行着的写入操作的内核部分。如果在写入操作结束前就调度了read()操作呢?就会产生数据不完整的状态。这样的bug非常难以找到。但是,怎样来处理这个问题呢?

基本上,我们需要确保在写方法返回前没有read方法能被执行。如果你曾经编写过一个多线程的应用程序,你可能见过同步原语(锁),如互斥锁或者信号。Linux也有这些,但有些细微的差别。内核代码可以运行进程上下文(用户空间代码的“代表”工作,就像我们使用的方法)和终端上下文(例如,一个IRQ处理线程)。如果你已经在进程上下文中和并且你已经得到了所需的锁,你只需要简单地睡眠和重试直到成功为止。在中断上下文时你不能处于休眠状态,因此代码会在一个循环中运行直到锁可用。关联原语被称为自旋锁,但在我们的环境中,一个简单的互斥锁 —— 在特定时间内只有唯一一个进程能“占有”的对象 —— 就足够了。处于性能方面的考虑,现实的代码可能也会使用读-写信号。

锁总是保护某些数据(在我们的环境中,是一个“struct buffer”实例),而且也常常会把它们嵌入到它们所保护的结构体中。因此,我们添加一个互斥锁(‘struct mutex lock’)到“struct buffer”中。我们也必须用mutex_init()来初始化互斥锁;buffer_alloc是用来处理这件事的好地方。使用互斥锁的代码也必须包含linux/mutex.h。

互斥锁很像交通信号灯 —— 要是司机不看它和不听它的,它就没什么用。因此,在对缓冲区做操作并在操作完成时释放它之前,我们需要更新reverse_read()和reverse_write()来获取互斥锁。让我们来看看read方法 —— write的工作原理相同:

bash/shell Code复制内容到剪贴板

我们在函数一开始就获取锁。mutex_lock_interruptible()要么得到互斥锁然后返回,要么让进程睡眠,直到有可用的互斥锁。就像前面一样,_interruptible后缀意味着睡眠可以由信号来中断。

bash/shell Code复制内容到剪贴板

下面是我们的“等待数据”循环。当获取互斥锁时,或者发生称之为“死锁”的情境时,不应该让进程睡眠。因此,如果没有数据,我们释放互斥锁并调用wait_event_interruptible()。当它返回时,我们重新获取互斥锁并像往常一样继续:

bash/shell Code复制内容到剪贴板

最后,当函数结束,或者在互斥锁被获取过程中发生错误时,互斥锁被解锁。重新编译模块(别忘了重新加载),然后再次进行测试。现在你应该没发现毁坏的数据了。

接下来是什么?

现在你已经尝试了一次内核黑客。我们刚刚为你揭开了这个话题的外衣,里面还有更多东西供你探索。我们的第一个模块有意识地写得简单一点,在从中学到的概念在更复杂的环境中也一样。并发、方法表、注册回调函数、使进程睡眠以及唤醒进程,这些都是内核黑客们耳熟能详的东西,而现在你已经看过了它们的运作。或许某天,你的内核代码也将被加入到主线Linux源代码树中 —— 如果真这样,请联系我们!

Linux内核模块编写详解的更多相关文章

  1. Linux启动过程详解(inittab、rc.sysinit、rcX.d、rc.local)

    启动第一步--加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它.这是因为BIOS中包含了CPU的相关信息.设备启动顺序信息.硬 ...

  2. Linux启动过程详解

    Linux启动过程详解 附上两张图,加深记忆 图1: 图2: 第一张图比较简洁明了,下面对第一张图的步骤进行详解: 加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的 ...

  3. Linux /dev目录详解和Linux系统各个目录的作用

    Linux /dev目录详解(转http://blog.csdn.net/maopig/article/details/7195048) 在linux下,/dev目录是很重要的,各种设备都在下面.下面 ...

  4. Linux crontab 命令详解

    在 Linux 中,任务可以被配置在指定的时间段.指定的日期.或系统平均载量低于指定的数量时自动运行.红帽企业 Linux 预配置了对重要系统任务的运行,以便使系统能够时时被更新.譬如,被 locat ...

  5. Linux crontab 命令详解(含配置文件路径)

    编辑/etc/crontab 文件配置cron cron 服务每分钟不仅要读一次/var/spool/cron内的所有文件,还需要读一次/etc/crontab,因此我们配置这个文件也能运用cron服 ...

  6. Linux串口编程详解(转)

    串口本身,标准和硬件 † 串口是计算机上的串行通讯的物理接口.计算机历史上,串口曾经被广泛用于连接计算机和终端设备和各种外部设备.虽然以太网接口和USB接口也是以一个串行流进行数据传送的,但是串口连接 ...

  7. Linux网络配置文件详解

    --Linux网络配置文件详解----------------------2013/10/03 目前在企业级服务器的Linux系统中,RHEL占有绝对的优势,不管是曾经在互联网公司还是在目前测试Vir ...

  8. Linux curl 命令详解

    命令概要 该命令设计用于在没有用户交互的情况下工作. curl 是一个工具,用于传输来自服务器或者到服务器的数据.「向服务器传输数据或者获取来自服务器的数据」 可支持的协议有(DICT.FILE.FT ...

  9. 红帽Linux故障定位技术详解与实例(3)

    红帽Linux故障定位技术详解与实例(3)   在线故障定位就是在故障发生时, 故障所处的操作系统环境仍然可以访问,故障处理人员可通过console, ssh等方式登录到操作系统上,在shell上执行 ...

随机推荐

  1. npm安装express 公司上网设置代理

    异常: 0 info it worked if it ends with ok1 verbose cli [ 'C:\\Program Files (x86)\\nodejs\\\\node.exe' ...

  2. Flash Builder 相关

    1.Flex SDK 4.1 兼容性 Flex SDK 4.1 兼容 Flash Builder 4.0 ,因此在 Flash Builder 4.0 中使用 4.1 SDK 时可以使用设计视图 Fl ...

  3. Dropdownlist中用viewmodel传值处理方法

    背景:MVC框架,页面使用razor语法,下拉框的话使用了@Html.DropDownList(),以前传值使用viewdata,但是我们老大说这个方式比较low,希望我可以使用viewmodel的方 ...

  4. 封装IP池和用户代理相应的类(python3)

    一.middlewares.py源代码: # -*- coding: utf-8 -*- # 导入随机模块 import random # 导入有关IP池有关的模块 from scrapy.contr ...

  5. 在Scrapy中使用IP池或用户代理(python3)

    一.创建Scrapy工程 scrapy startproject 工程名 二.进入工程目录,根据爬虫模板生成爬虫文件 scrapy genspider -l # 查看可用模板 scrapy gensp ...

  6. 巨蟒python全栈开发-第9天 初识函数

    一.今日主要内容总览(重点) 1.什么是函数? f(x)=x+1 y=x+1 函数是对功能或者动作的封装2.函数的语法和定义 def 函数名(): 函数体 调用:函数名()3.关于函数的返回值 ret ...

  7. Audit File Delete

    OPEN 4656 ACCESS   4663 AN OBJECT WAS DELETED 4660 CLOSE 4658 1537 = Delete 1538 = Read_CONTROL 1541 ...

  8. 判断 checkbox 是否选中以及 设置checkbox选中

    //判断checkbox 是否选中 $("#id").is(":checked");//选中,返回true,没选中,返回false //设置checkbox为选 ...

  9. 在.NET中读取嵌入和使用资源文件的方法

    转http://www.jb51.net/article/84660.htm 本文分别介绍了使用GetManifestResourceStream读取嵌入资源,和使用. resx资源文件嵌入资源,希望 ...

  10. stopPropagation(), preventDefault() , return false 事件

    因为有父, 子节点同在, 因为有监听事件和浏览器默认动作之分. 使用 JavaScript 时为了达到预期效果经常需要阻止事件和动作执行. 一般我们会用到三种方法, 分别是 stopPropagati ...