标签 模块 下的文章

开源爱好者们对他们所喜爱的 Linux 内核模块进行了评价。

 title=

Linux 内核今年就要满 30 岁了! 如果你像我们一样对此特别重视,那么让我们本周用几个特别的文章来庆祝 Linux。

今天,我们先来看看来自社区对“你不能没有哪个 Linux 内核模块?为什么?”的回答,让我们听听这 10 位爱好者是怎么说的。

1

我猜一些内核开发者听到我的回答后会尖叫着跑开。不过,我还是在这里列出了两个最具争议性的模块:

  • 第一个是 NVIDIA,因为我的工作笔记本和个人台式机上都有 NVIDIA 显卡。
  • 另一个可能产生的仇恨较少。VMware 的 VMNET 和 VMMON 模块,以便能够运行 VMware Workstation。

Peter Czanik

2

我最喜欢的是 zram 模块。它在内存中创建了一个压缩块设备,然后它可以作为交换分区使用。在内存有限的情况下(例如,在虚拟机上),还有如果你担心频繁的 I/O 操作会磨损你的 SSD 或者甚至更糟糕的基于闪存的存储,那么使用基于 zram 的交换分区是非常理想的。

Stephan Avenwedde

3

最有用的内核模块无疑是 snd-hda-intel,因为它支持大多数集成声卡。我可以一边听音乐,一边在 Linux 桌面上编码一个音频编曲器。

Joël Krähemann

4

如果没有我用 Broadcom 文件生成的 kmod-wl,我的笔记本就没有价值了。我有时会收到关于内核污染的信息,但没有无线网络的笔记本电脑有什么用呢?

Gregory Pittman

5

我不能没有蓝牙。没有它,我的鼠标、键盘、扬声器和耳机除了用来挡住门板还有啥用?

Gary Smith

6

我要冒昧地说 都是。 说真的,我们已经到了随机拿一块硬件,插入它,它就可以工作的地步。

  • USB 串行适配器能正常工作
  • 显卡可以使用(尽管可能不是最好的)
  • 网卡正常工作
  • 声卡正常工作

所有这些模块整体带来大量可以工作的驱动程序,令人印象深刻。我记得在过去那些糟糕的日子里,我们曾经大喊 xrandr 魔法字符串才能来使投影仪工作。而现在,是的,当设备基本不能正常工作时,才真的罕见。

如果我不得不把它归结为一个,那就是 raid6。

John 'Warthog9' Hawley

7

对于这个问题,我想回到 20 世纪 90 年代末。我是一家小公司的 Unix 系统管理员(兼任 IS 经理)。我们的磁带备份系统死了,由于“小公司”预算有限,我们没有急于更换或现场维修。所以我们必须得把它送去维修。

在那两个星期里,我们没有办法进行磁带备份。没有一个系统管理员愿意处于这种境地。

但后来我想起了读过的 如何使用软盘磁带机,我们刚好有一台刚换下来的塔式电脑,它有一个软盘磁带机。

于是我用 Linux 重新安装了它,设置了 ftape 内核驱动模块,进行了一些备份/恢复测试,然后将我们最重要的备份运行到 QIC 磁带上。在这两个星期里,我们依靠 ftape 备份重要数据。

所以,对于那些让软盘磁带机在 1990 年代的 Linux 上工作的无名英雄,你真是太厉害了!

Jim Hall

8

嗯,这很简单。是 kvm 内核模块。就个人而言,我无法想象在没有虚拟机的情况下完成日常工作。我愿意相信我们大多数人都是这样。kvm 模块在使 Linux 成为云战略的核心方面也发挥了很大作用。

Gaurav Kamathe

9

对我来说,是 dm-crypt,它是用于 LUKS 的。参见:

知道别人无法看到你的磁盘上的内容是非常棒的,例如,如果你的笔记本丢失或被盗时。

Maximilian Kolb

10

对于密码学基础,很难超越 crypto 模块和它的 C API,它是如此简洁明了。

在日常生活中,还有什么比蓝牙提供的即插即用更有价值的吗?

Marty Kalin

在评论中与我们分享。你的生活中不能没有什么 Linux 内核模块?


via: https://opensource.com/article/21/8/linux-kernel-module

作者:Jen Wike Huger 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

lsmod 命令能够告诉你当前系统上加载了哪些内核模块,以及关于使用它们的一些有趣的细节。

Rob Oo (CC BY 2.0)

什么是 Linux 内核模块?

内核模块是可以根据需要加载到内核中或从内核中卸载的代码块,因此无需重启就可以扩展内核的功能。事实上,除非用户使用类似 lsmod 这样的命令来查询模块信息,否则用户不太可能知道内核发生的任何变化。

需要知道的重要一点是,在你的 Linux 系统上总会有很多可用的模块,并且如果你可以深入其中了解到很多细节。

lsmod 的主要用途之一是在系统不能正常工作时检查模块。然而,大多数情况下,模块会根据需要加载的,而且用户不需要知道它们如何运作。

显示内核模块

显示内核模块最简单的方法是使用 lsmod 命令。虽然这个命令包含了很多细节,但输出却是非常用户友好。

$ lsmod
Module                  Size  Used by
snd_hda_codec_realtek 114688  1
snd_hda_codec_generic  77824  1 snd_hda_codec_realtek
ledtrig_audio          16384  2 snd_hda_codec_generic,snd_hda_codec_realtek
snd_hda_codec_hdmi     53248  1
snd_hda_intel          40960  2
snd_hda_codec         131072  4 snd_hda_codec_generic,snd_hda_codec_hdmi,snd_hda_intel
                                ,snd_hda_codec_realtek
snd_hda_core           86016  5 snd_hda_codec_generic,snd_hda_codec_hdmi,snd_hda_intel
                                ,snd_hda_codec,snd_hda_codec_realtek
snd_hwdep              20480  1 snd_hda_codec
snd_pcm               102400  4 snd_hda_codec_hdmi,snd_hda_intel,snd_hda_codec,snd_hda
                                _core
snd_seq_midi           20480  0
snd_seq_midi_event     16384  1 snd_seq_midi
dcdbas                 20480  0
snd_rawmidi            36864  1 snd_seq_midi
snd_seq                69632  2 snd_seq_midi,snd_seq_midi_event
coretemp               20480  0
snd_seq_device         16384  3 snd_seq,snd_seq_midi,snd_rawmidi
snd_timer              36864  2 snd_seq,snd_pcm
kvm_intel             241664  0
kvm                   626688  1 kvm_intel
radeon               1454080  10
irqbypass              16384  1 kvm
joydev                 24576  0
input_leds             16384  0
ttm                   102400  1 radeon
drm_kms_helper        180224  1 radeon
drm                   475136  13 drm_kms_helper,radeon,ttm
snd                    81920  15 snd_hda_codec_generic,snd_seq,snd_seq_device,snd_hda
                                 _codec_hdmi,snd_hwdep,snd_hda_intel,snd_hda_codec,snd
                                 _hda_codec_realtek,snd_timer,snd_pcm,snd_rawmidi
i2c_algo_bit           16384  1 radeon
fb_sys_fops            16384  1 drm_kms_helper
syscopyarea            16384  1 drm_kms_helper
serio_raw              20480  0
sysfillrect            16384  1 drm_kms_helper
sysimgblt              16384  1 drm_kms_helper
soundcore              16384  1 snd
mac_hid                16384  0
sch_fq_codel           20480  2
parport_pc             40960  0
ppdev                  24576  0
lp                     20480  0
parport                53248  3 parport_pc,lp,ppdev
ip_tables              28672  0
x_tables               40960  1 ip_tables
autofs4                45056  2
raid10                 57344  0
raid456               155648  0
async_raid6_recov      24576  1 raid456
async_memcpy           20480  2 raid456,async_raid6_recov
async_pq               24576  2 raid456,async_raid6_recov
async_xor              20480  3 async_pq,raid456,async_raid6_recov
async_tx               20480  5 async_pq,async_memcpy,async_xor,raid456,async_raid6_re
                                cov
xor                    24576  1 async_xor
raid6_pq              114688  3 async_pq,raid456,async_raid6_recov
libcrc32c              16384  1 raid456
raid1                  45056  0
raid0                  24576  0
multipath              20480  0
linear                 20480  0
hid_generic            16384  0
psmouse               151552  0
i2c_i801               32768  0
pata_acpi              16384  0
lpc_ich                24576  0
usbhid                 53248  0
hid                   126976  2 usbhid,hid_generic
e1000e                245760  0
floppy                 81920  0

在上面的输出中:

  • Module 显示每个模块的名称
  • Size 显示每个模块的大小(并不是它们占的内存大小)
  • Used by 显示每个模块被使用的次数和使用它们的模块

显然,这里有很多模块。加载的模块数量取决于你的系统和版本以及正在运行的内容。我们可以这样计数:

$ lsmod | wc -l
67

要查看系统中可用的模块数(不止运行当中的),试试这个命令:

$ modprobe -c | wc -l
41272

与内核模块相关的其他命令

Linux 提供了几条用于罗列、加载及卸载、测试,以及检查模块状态的命令。

  • depmod —— 生成 modules.dep 和映射文件
  • insmod —— 一个往 Linux 内核插入模块的程序
  • lsmod —— 显示 Linux 内核中模块状态
  • modinfo —— 显示 Linux 内核模块信息
  • modprobe —— 添加或移除 Linux 内核模块
  • rmmod —— 一个从 Linux 内核移除模块的程序

显示内置的内核模块

正如前文所说,lsmod 命令是显示内核模块最方便的命令。然而,也有其他方式可以显示它们。modules.builtin 文件中列出了所有构建在内核中的模块,在 modprobe 命令尝试添加文件中的模块时会使用它。注意,以下命令中的 $(uname -r) 提供了内核版本的名称。

$ more /lib/modules/$(uname -r)/modules.builtin | head -10
kernel/arch/x86/crypto/crc32c-intel.ko
kernel/arch/x86/events/intel/intel-uncore.ko
kernel/arch/x86/platform/intel/iosf_mbi.ko
kernel/mm/zpool.ko
kernel/mm/zbud.ko
kernel/mm/zsmalloc.ko
kernel/fs/binfmt_script.ko
kernel/fs/mbcache.ko
kernel/fs/configfs/configfs.ko
kernel/fs/crypto/fscrypto.ko

你可以使用 modinfo 获得一个模块的更多细节,虽然没有对模块提供的服务的简单说明。下面输出内容中省略了冗长的签名。

$ modinfo floppy | head -16
filename:       /lib/modules/5.0.0-13-generic/kernel/drivers/block/floppy.ko
alias:          block-major-2-*
license:        GPL
author:         Alain L. Knaff
srcversion:     EBEAA26742DF61790588FD9
alias:          acpi*:PNP0700:*
alias:          pnp:dPNP0700*
depends:
retpoline:      Y
intree:         Y
name:           floppy
vermagic:       5.0.0-13-generic SMP mod_unload
sig_id:         PKCS#7
signer:
sig_key:
sig_hashalgo:   md4

你可以使用 modprobe 命令加载或卸载模块。使用下面这条命令,你可以找到特定模块关联的内核对象:

$ find /lib/modules/$(uname -r) -name floppy*
/lib/modules/5.0.0-13-generic/kernel/drivers/block/floppy.ko

如果你想要加载模块,你可以使用这个命令:

$ sudo modprobe floppy

总结

很明显,内核模块的加载和卸载非常重要。它使得 Linux 系统比使用通用内核运行时更加灵活和高效。这同样意味着你可以进行重大更改而无需重启,例如添加硬件。


via: https://www.networkworld.com/article/3391362/looking-into-linux-modules.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:LazyWolfLin 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

找到并装载内核模块以解决外设问题。

本文来自 Manning 出版的 Linux in Action 的第 15 章。

Linux 使用内核模块管理硬件外设。 我们来看看它是如何工作的。

运行中的 Linux 内核是您不希望被破坏的东西之一。毕竟,内核是驱动计算机所做的一切工作的软件。考虑到在一个运行的系统上必须同时管理诸多细节,最好能让内核尽可能的减少分心,专心的完成它的工作。但是,如果对计算环境进行任何微小的更改都需要重启整个系统,那么插入一个新的网络摄像头或打印机就可能会严重影响您的工作流程。每次添加设备时都必须重新启动,以使系统识别它,这效率很低。

为了在稳定性和可用性之间达成有效的平衡,Linux 将内核隔离,但是允许您通过可加载内核模块 (LKM) 实时添加特定的功能。如下图所示,您可以将模块视为软件的一部分,它告诉内核在哪里找到一个设备以及如何使用它。反过来,内核使设备对用户和进程可用,并监视其操作。

 title=

内核模块充当设备和 Linux 内核之间的转换器。

虽然你可以自己编写模块来完全按照你喜欢的方式来支持一个设备,但是为什么要这样做呢?Linux 模块库已经非常强大,通常不需要自己去实现一个模块。 而绝大多数时候,Linux 会自动加载新设备的模块,而您甚至不知道它。

不过,有时候,出于某种原因,它本身并不会自动进行。(你肯定不想让那个招聘经理不耐烦地一直等待你的笑脸加入视频面试。)为了帮助你解决问题,你需要更多地了解内核模块,特别是,如何找到运行你的外设的实际模块,然后如何手动激活它。

查找内核模块

按照公认的约定,内核模块是位于 /lib/modules/ 目录下的具有 .ko(内核对象)扩展名的文件。 然而,在你找到这些文件之前,你还需要选择一下。因为在引导时你需要从版本列表中选择其一加载,所以支持您选择的特定软件(包括内核模块)必须存在某处。 那么,/lib/modules/ 就是其中之一。 你会发现目录里充满了每个可用的 Linux 内核版本的模块; 例如:

$ ls /lib/modules
4.4.0-101-generic
4.4.0-103-generic
4.4.0-104-generic

在我的电脑上,运行的内核是版本号最高的版本(4.4.0-104-generic),但不能保证这对你来说是一样的(内核经常更新)。 如果您将要在一个运行的系统上使用模块完成一些工作的话,你需要确保您找到正确的目录树。

好消息:有一个可靠的窍门。相对于通过名称来识别目录,并希望能够找到正确的目录,你可以使用始终指向使用的内核名称的系统变量。 您可以使用 uname -r-r 从系统信息中指定通常显示的内核版本号)来调用该变量:

$ uname -r
4.4.0-104-generic

通过这些信息,您可以使用称为命令替换的过程将 uname 并入您的文件系统引用中。 例如,要导航到正确的目录,您需要将其添加到 /lib/modules 。 要告诉 Linux “uname” 不是一个文件系统中的位置,请将 uname 部分用反引号括起来,如下所示:

$ ls /lib/modules/`uname -r`
build   modules.alias        modules.dep      modules.softdep
initrd  modules.alias.bin    modules.dep.bin  modules.symbols
kernel  modules.builtin      modules.devname  modules.symbols.bin
misc    modules.builtin.bin  modules.order    vdso

你可以在 kernel/ 目录下的子目录中找到大部分模块。 花几分钟时间浏览这些目录,了解事物的排列方式和可用内容。 这些文件名通常会让你知道它们是什么。

$ ls /lib/modules/`uname -r`/kernel
arch  crypto  drivers  fs  kernel  lib  mm 
net  sound  ubuntu  virt  zfs

这是查找内核模块的一种方法;实际上,这是一种快速的方式。 但这不是唯一的方法。 如果你想获得完整的集合,你可以使用 lsmod 列出所有当前加载的模块以及一些基本信息。 这个截断输出的第一列(在这里列出的太多了)是模块名称,后面是文件大小和数量,然后是每个模块的名称:

$ lsmod
[...]
vboxdrv          454656  3 vboxnetadp,vboxnetflt,vboxpci
rt2x00usb        24576  1 rt2800usb
rt2800lib        94208  1 rt2800usb
[...]

到底有多少?好吧,我们再运行一次 lsmod ,但是这一次将输出管道输送到 wc -l 看一下一共多少行:

$ lsmod | wc -l
113

这是已加载的模块。 总共有多少个? 运行 modprobe -c 并计算这些行将给我们这个数字:

$ modprobe -c | wc -l
33350

有 33,350 个可用模块!? 看起来好像有人多年来一直在努力为我们提供软件来驱动我们的物理设备。

注意:在某些系统中,您可能会遇到自定义的模块,这些模块要么在 /etc/modules 文件中使用独特的条目进行引用,要么在 /etc/modules-load.d/ 下的配置文件中。这些模块很可能是本地开发项目的产物,可能涉及前沿实验。不管怎样,知道你看到的是什么总是好的。

这就是如何找到模块的方法。 如果出于某种原因,它不会自行加载,您的下一个工作就是弄清楚如何手动加载未激活的模块。

手动加载内核模块

在加载内核模块之前,逻辑上您必须确认它存在。在这之前,你需要知道它叫什么。要做到这一点,有时需要兼有魔法和运气以及在线文档作者的辛勤工作的帮助。

我将通过描述一段时间前遇到的问题来说明这个过程。在一个晴朗的日子里,出于某种原因,笔记本电脑上的 WiFi 接口停止工作了。就这样。也许是软件升级把它搞砸了。谁知道呢?我运行了 lshw -c network ,得到了这个非常奇怪的信息:

network UNCLAIMED
    AR9485 Wireless Network Adapter

Linux 识别到了接口(Atheros AR9485),但将其列为未声明。 那么,正如他们所说的那样,“当情况变得严峻时,就会在互联网上进行艰难的搜索。” 我搜索了一下 atheros ar9 linux 模块,在浏览了一页又一页五年前甚至是十年前的页面后,它们建议我自己写个模块或者放弃吧,然后我终于发现(最起码 Ubuntu 16.04)有一个可以工作的模块。 它的名字是 ath9k 。

是的! 这场战斗胜券在握!向内核添加模块比听起来容易得多。 要仔细检查它是否可用,可以针对模块的目录树运行 find,指定 -type f 来告诉 Linux 您正在查找文件,然后将字符串 ath9k 和星号一起添加以包含所有以你的字符串打头的文件:

$ find /lib/modules/$(uname -r) -type f -name ath9k*
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_common.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_htc.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_hw.ko

再一步,加载模块:

# modprobe ath9k

就是这样。无启动,没烦恼。

这里还有一个示例,向您展示如何使用已经崩溃的运行模块。曾经有一段时间,我使用罗技网络摄像头和一个特定的软件会使摄像头在下次系统启动前无法被任何其他程序访问。有时我需要在不同的应用程序中打开相机,但没有时间关机重新启动。(我运行了很多应用程序,在引导之后将它们全部准备好需要一些时间。)

由于这个模块可能是运行的,所以使用 lsmod 来搜索 video 这个词应该给我一个关于相关模块名称的提示。 实际上,它比提示更好:用 video 这个词描述的唯一模块是 uvcvideo(如下所示):

$ lsmod | grep video
uvcvideo               90112  0
videobuf2_vmalloc      16384  1 uvcvideo
videobuf2_v4l2         28672  1 uvcvideo
videobuf2_core         36864  2 uvcvideo,videobuf2_v4l2
videodev              176128  4 uvcvideo,v4l2_common,videobuf2_core,videobuf2_v4l2
media                  24576  2 uvcvideo,videodev

有可能是我自己的操作导致了崩溃,我想我可以挖掘更深一点,看看我能否以正确的方式解决问题。但结果你知道的;有时你不关心理论,只想让设备工作。 所以我用 rmmod 杀死了 uvcvideo 模块,然后用 modprobe 重新启动它,一切都好:

# rmmod uvcvideo
# modprobe uvcvideo

再一次:不重新启动。没有其他的后续影响。


via: https://opensource.com/article/18/5/how-load-or-unload-linux-kernel-module

作者:David Clinton 选题:lujun9972 译者:amwps290 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

更新(2016/10/30):我写完这篇文章之后,我在这个基准测试中发了一个错误,会导致 Rollup 比它预期的看起来要好一些。不过,整体结果并没有明显的不同(Rollup 仍然击败了 Browserify 和 Webpack,虽然它并没有像 Closure 十分好),所以我只是更新了图表。该基准测试包括了 RequireJS 和 RequireJS Almond 打包器,所以文章中现在也包括了它们。要看原始帖子,可以查看历史版本

大约一年之前,我在将一个大型 JavaScript 代码库重构为更小的模块时发现了 Browserify 和 Webpack 中一个令人沮丧的事实:

“代码越模块化,代码体积就越大。:< ”

  • Nolan Lawson

过了一段时间,Sam Saccone 发布了一些关于 TumblrImgur 页面加载性能的出色的研究。其中指出:

“超过 400 ms 的时间单纯的花费在了遍历 Browserify 树上。”

  • Sam Saccone

在本篇文章中,我将演示小模块可能会根据你选择的 打包器 bundler 模块系统 module system 而出现高得惊人的性能开销。此外,我还将解释为什么这种方法不但影响你自己代码的模块,也会影响依赖项中的模块,这也正是第三方代码在性能开销上很少提及的方面。

网页性能

一个页面中包含的 JavaScript 脚本越多,页面加载也将越慢。庞大的 JavaScript 包会导致浏览器花费更多的时间去下载、解析和执行,这些都将加长载入时间。

即使当你使用如 Webpack code splitting、Browserify factor bundles 等工具将代码分解为多个包,该开销也仅仅是被延迟到页面生命周期的晚些时候。JavaScript 迟早都将有一笔开销。

此外,由于 JavaScript 是一门动态语言,同时流行的 CommonJS 模块也是动态的,所以这就使得在最终分发给用户的代码中剔除无用的代码变得异常困难。譬如你可能只使用到 jQuery 中的 $.ajax,但是通过载入 jQuery 包,你将付出整个包的代价。

JavaScript 社区对这个问题提出的解决办法是提倡 小模块 的使用。小模块不仅有许多 美好且实用的好处 如易于维护,易于理解,易于集成等,而且还可以通过鼓励包含小巧的功能而不是庞大的库来解决之前提到的 jQuery 的问题。

所以在小模块下,你将不需要这样:

var _ = require('lodash')
_.uniq([1,2,2,3])

而是可以如此:

var uniq = require('lodash.uniq')
uniq([1,2,2,3])

包与模块

需要强调的是这里我提到的“模块”并不同于 npm 中的“包”的概念。当你从 npm 安装一个包时,它会将该模块通过公用 API 展现出来,但是在这之下其实是一个许多模块的聚合物。

例如,我们来看一个包 is-array,它没有别的依赖,并且只包含 一个 JavaScript 文件,所以它只有一个模块。这算是足够简单的。

现在来看一个稍微复杂一点的包,如 once。它有一个依赖的包 wrappy 包都各自包含一个模块,所以总模块数为 2。至此,也还算好。

现在来一起看一个更为令人迷惑的例子:qs。因为它没有依赖的包,所以你可能就认为它只有一个模块,然而事实上,它有四个模块!

你可以用一个我写的工具 browserify-count-modules 来统计一个 Browserify 包的总模块数:

$ npm install qs
$ browserify node_modules/qs | browserify-count-modules
4

这说明了一个包可以包含一个或者多个模块。这些模块也可以依赖于其他的包,而这些包又将附带其自己所依赖的包与模块。由此可以确定的事就是任何一个包将包含至少一个模块。

模块膨胀

一个典型的网页应用中会包含多少个模块呢?我在一些流行的使用 Browserify 的网站上运行 browserify-count-moduleson 并且得到了以下结果:

顺带一提,我写过的最大的开源站点 Pokedex.org 包含了 4 个包,共 311 个模块。

让我们先暂时忽略这些 JavaScript 包的实际大小,我认为去探索一下一定数量的模块本身开销会是一件有意思的事。虽然 Sam Saccone 的文章 “2016 年 ES2015 转译的开销” 已经广为流传,但是我认为他的结论还未到达足够深度,所以让我们挖掘的稍微再深一点吧。

测试环节!

我构造了一个能导入 100、1000 和 5000 个其他小模块的测试模块,其中每个小模块仅仅导出一个数字。而父模块则将这些数字求和并记录结果:

// index.js
var total = 0
total += require('./module_0')
total += require('./module_1')
total += require('./module_2')
// etc.
console.log(total)


// module_1.js
module.exports = 1

我测试了五种打包方法:Browserify、带 bundle-collapser 插件的 Browserify、Webpack、Rollup 和 Closure Compiler。对于 Rollup 和 Closure Compiler 我使用了 ES6 模块,而对于 Browserify 和 Webpack 则用的是 CommonJS,目的是为了不涉及其各自缺点而导致测试的不公平(由于它们可能需要做一些转译工作,如 Babel 一样,而这些工作将会增加其自身的运行时间)。

为了更好地模拟一个生产环境,我对所有的包采用带 -mangle-compress 参数的 Uglify ,并且使用 gzip 压缩后通过 GitHub Pages 用 HTTPS 协议进行传输。对于每个包,我一共下载并执行 15 次,然后取其平均值,并使用 performance.now() 函数来记录载入时间(未使用缓存)与执行时间。

包大小

在我们查看测试结果之前,我们有必要先来看一眼我们要测试的包文件。以下是每个包最小处理后但并未使用 gzip 压缩时的体积大小(单位:Byte):

100 个模块1000 个模块5000 个模块
browserify798279987419985
browserify-collapsed578657991309982
webpack395439055203052
rollup671697138968
closure758795843955
100 个模块1000 个模块5000 个模块
browserify16491380064513
browserify-collapsed14641190356335
webpack693502726363
rollup300214511510
closure302214011789

Browserify 和 Webpack 的工作方式是隔离各个模块到各自的函数空间,然后声明一个全局载入器,并在每次 require() 函数调用时定位到正确的模块处。下面是我们的 Browserify 包的样子:

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o

而 Rollup 和 Closure 包看上去则更像你亲手写的一个大模块。这是 Rollup 打包的包:

(function () {
        'use strict';
        var total = 0
        total += 0
        total += 1
        total += 2
// etc.

如果你清楚在 JavaScript 中使用嵌套函数与在关联数组查找一个值的固有开销, 那么你将很容易理解出现以下测试的结果的原因。

测试结果

我选择在搭载 Android 5.1.1 与 Chrome 52 的 Nexus 5(代表中低端设备)和运行 iOS 9 的第 6 代 iPod Touch(代表高端设备)上进行测试。

这是 Nexus 5 下的测试结果(查看表格):

Nexus 5 结果

这是 iPod Touch 下的测试结果(查看表格):

iPod Touch 结果

在 100 个模块时,各包的差异是微不足道的,但是一旦模块数量达到 1000 个甚至 5000 个时,差异将会变得非常巨大。iPod Touch 在不同包上的差异并不明显,而对于具有一定年代的 Nexus 5 来说,Browserify 和 Webpack 明显耗时更多。

与此同时,我发现有意思的是 Rollup 和 Closure 的运行开销对于 iPod 而言几乎可以忽略,并且与模块的数量关系也不大。而对于 Nexus 5 来说,运行的开销并非完全可以忽略,但 Rollup/Closure 仍比 Browserify/Webpack 低很多。后者若未在几百毫秒内完成加载则将会占用主线程的好几帧的时间,这就意味着用户界面将冻结并且等待直到模块载入完成。

值得注意的是前面这些测试都是在千兆网速下进行的,所以在网络情况来看,这只是一个最理想的状况。借助 Chrome 开发者工具,我们可以认为地将 Nexus 5 的网速限制到 3G 水平,然后来看一眼这对测试产生的影响(查看表格):

Nexus 5 3G 结果

一旦我们将网速考虑进来,Browserify/Webpack 和 Rollup/Closure 的差异将变得更为显著。在 1000 个模块规模(接近于 Reddit 1050 个模块的规模)时,Browserify 花费的时间比 Rollup 长大约 400 毫秒。然而 400 毫秒已经不是一个小数目了,正如 Google 和 Bing 指出的,亚秒级的延迟都会 对用户的参与产生明显的影响

还有一件事需要指出,那就是这个测试并非测量 100 个、1000 个或者 5000 个模块的每个模块的精确运行时间。因为这还与你对 require() 函数的使用有关。在这些包中,我采用的是对每个模块调用一次 require() 函数。但如果你每个模块调用了多次 require() 函数(这在代码库中非常常见)或者你多次动态调用 require() 函数(例如在子函数中调用 require() 函数),那么你将发现明显的性能退化。

Reddit 的移动站点就是一个很好的例子。虽然该站点有 1050 个模块,但是我测量了它们使用 Browserify 的实际执行时间后发现比“1000 个模块”的测试结果差好多。当使用那台运行 Chrome 的 Nexus 5 时,我测出 Reddit 的 Browserify require() 函数耗时 2.14 秒。而那个“1000 个模块”脚本中的等效函数只需要 197 毫秒(在搭载 i7 处理器的 Surface Book 上的桌面版 Chrome,我测出的结果分别为 559 毫秒与 37 毫秒,虽然给出桌面平台的结果有些令人惊讶)。

这结果提示我们有必要对每个模块使用多个 require() 函数的情况再进行一次测试。不过,我并不认为这对 Browserify 和 Webpack 会是一个公平的测试,因为 Rollup 和 Closure 都会将重复的 ES6 库导入处理为一个的顶级变量声明,同时也阻止了顶层空间以外的其他区域的导入。所以根本上来说,Rollup 和 Closure 中一个导入和多个导入的开销是相同的,而对于 Browserify 和 Webpack,运行开销随 require() 函数的数量线性增长。

为了我们这个分析的目的,我认为最好假设模块的数量是性能的短板。而事实上,“5000 个模块”也是一个比“5000 个 require() 函数调用”更好的度量标准。

结论

首先,bundle-collapser 对 Browserify 来说是一个非常有用的插件。如果你在产品中还没使用它,那么你的包将相对来说会略大且运行略慢(虽然我得承认这之间的差异非常小)。另一方面,你还可以转换到 Webpack 以获得更快的包而不需要额外的配置(其实我非常不愿意这么说,因为我是个顽固的 Browserify 粉)。

不管怎样,这些结果都明确地指出 Webpack 和 Browserify 相较 Rollup 和 Closure Compiler 而言表现都稍差,并且性能差异随着模块大小的增大而增大。不幸的是,我并不确定 Webpack 2 是否能解决这些问题,因为尽管他们将 从 Rollup 中借鉴一些想法,但是看起来他们的关注点更多在于 tree-shaking 方面 而不是在于 scope-hoisting 方面。(更新:一个更好的名字称为 内联 inlining ,并且 Webpack 团队 正在做这方面的工作。)

给出这些结果之后,我对 Closure Compiler 和 Rollup 在 JavaScript 社区并没有得到过多关注而感到惊讶。我猜测或许是因为(前者)需要依赖 Java,而(后者)仍然相当不成熟并且未能做到开箱即用(详见 Calvin’s Metcalf 的评论 中作的不错的总结)。

即使没有足够数量的 JavaScript 开发者加入到 Rollup 或 Closure 的队伍中,我认为 npm 包作者们也已准备好了去帮助解决这些问题。如果你使用 npm 安装 lodash,你将会发其现主要的导入是一个巨大的 JavaScript 模块,而不是你期望的 Lodash 的 超模块 hyper-modular 特性(require('lodash/uniq')require('lodash.uniq') 等等)。对于 PouchDB,我们做了一个类似的声明以 使用 Rollup 作为预发布步骤,这将产生对于用户而言尽可能小的包。

同时,我创建了 rollupify 来尝试将这过程变得更为简单一些,只需拖动到已存在的 Browserify 工程中即可。其基本思想是在你自己的项目中使用 导入 import 导出 export (可以使用 cjs-to-es6 来帮助迁移),然后使用 require() 函数来载入第三方包。这样一来,你依旧可以在你自己的代码库中享受所有模块化的优点,同时能导出一个适当大小的大模块来发布给你的用户。不幸的是,你依旧得为第三方库付出一些代价,但是我发现这是对于当前 npm 生态系统的一个很好的折中方案。

所以结论如下:一个大的 JavaScript 包比一百个小 JavaScript 模块要快。尽管这是事实,我依旧希望我们社区能最终发现我们所处的困境————提倡小模块的原则对开发者有利,但是对用户不利。同时希望能优化我们的工具,使得我们可以对两方面都有利。

福利时间!三款桌面浏览器

通常来说我喜欢在移动设备上运行性能测试,因为在这里我们能更清楚的看到差异。但是出于好奇,我也分别在一台搭载 i7 的 Surface Book 上的 Chrome 52、Edge 14 和 Firefox 48 上运行了测试。这分别是它们的测试结果:

Chrome 52 (查看表格)

Chrome 结果

Edge 14 (查看表格)

Edge 结果

Firefox 48 (查看表格)

Firefox 结果

我在这些结果中发现的有趣的地方如下:

  1. bundle-collapser 总是与 slam-dunk 完全不同。
  2. Rollup 和 Closure 的下载时间与运行时间之比总是非常高,它们的运行时间基本上微不足道。ChakraCore 和 SpiderMonkey 运行最快,V8 紧随其后。

如果你的 JavaScript 非常大并且是延迟加载,那么第二点将非常重要。因为如果你可以接受等待网络下载的时间,那么使用 Rollup 和 Closure 将会有避免界面线程冻结的优点。也就是说,它们将比 Browserify 和 Webpack 更少出现界面阻塞。

更新:在这篇文章的回应中,JDD 已经 给 Webpack 提交了一个 issue。还有 一个是给 Browserify 的

更新 2:Ryan Fitzer 慷慨地增加了 RequireJS 和包含 Almond 的 RequireJS 的测试结果,两者都是使用 AMD 而不是 CommonJS 或者 ES6。

测试结果表明 RequireJS 具有 最大的包大小 但是令人惊讶的是它的运行开销 与 Rollup 和 Closure 非常接近。这是在运行 Chrome 52 的 Nexus 5 下限制网速为 3G 的测试结果:

Nexus 5 (3G) RequireJS 结果

更新 3: 我写了一个 optimize-js ,它会减少一些函数内的函数的解析成本。


via: https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/

作者:Nolan 译者:Yinr 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

提问:我想要知道Linux系统中内核内置的模块,以及每个模块有哪些参数。有什么方法可以得到内置模块和设备驱动的列表,以及它们的详细信息呢?

现代Linux内核正在随着时间变化而迅速增长,以支持大量的硬件、文件系统和网络功能。在此期间,“可加载模块(loadable kernel modules,[LKM])”的引入防止内核变得越来越臃肿,以及在不同的环境中灵活地扩展功能及硬件支持,而不必重新构建内核。

最新的Linux发行版的内核只带了相对较小的“内置模块(built-in modules)”,其余的特定硬件驱动或者自定义功能作为“可加载模块”来让你选择地加载或卸载。

内置模块被静态地编译进了内核。不像可加载内核模块可以动态地使用modprobeinsmodrmmodmodinfo或者lsmod等命令地加载、卸载、查询模块,内置的模块总是在启动时就加载进了内核,不会被这些命令管理。

找出内置模块列表

要得到内置模块列表,运行下面的命令。

$ cat /lib/modules/$(uname -r)/modules.builtin 

你也可以用下面的命令来查看有哪些内置模块:

找出内置模块参数

每个内核模块无论是内置的还是可加载的都有一系列的参数。对于可加载模块,modinfo命令可以显示它们的参数信息。然而这个命令对内置模块没有用。你会得到下面的错误。

modinfo: ERROR: Module XXXXXX not found.

如果你想要查看内置模块的参数,以及它们的值,你可以在 /sys/module 下检查它们的内容。

在 /sys/module目录下,你可以找到内核模块(包含内置和可加载的)命名的子目录。进入每个模块目录,这里有个“parameters”目录,列出了这个模块所有的参数。

比如你要找出tcp\_cubic(内核默认的TCP实现)模块的参数。你可以这么做:

$ ls /sys/module/tcp_cubic/parameters 

接着阅读这个文件查看每个参数的值。

$ cat /sys/module/tcp_cubic/parameters/tcp_friendliness 


via: http://ask.xmodulo.com/find-information-builtin-kernel-modules-linux.html

作者:Dan Nanni 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,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上——这里我们将看其中的一些片段。开始时,我们先要包含一些常见的文件头,并用预定义的宏来描述模块:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <[email protected]>");
MODULE_DESCRIPTION("In-kernel phrase reverser");

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

什么时候不该写内核模块

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

  • 你要开发一个USB驱动 —— 请查看libusb
  • 你要开发一个文件系统 —— 试试FUSE
  • 你在扩展Netfilter —— 那么libnetfilter\_queue对你有所帮助。

通常,内核里面代码的性能会更好,但是对于许多项目而言,这点性能丢失并不严重。

由于内核编程总是异步的,没有一个main()函数来让Linux顺序执行你的模块。取而代之的是,你要为各种事件提供回调函数,像这个:

static int __init reverse_init(void)
{
    printk(KERN_INFO "reverse device has been registered\n");
    return 0;
}

static void __exit reverse_exit(void)
{
    printk(KERN_INFO "reverse device has been unregistered\n");
}

module_init(reverse_init);
module_exit(reverse_exit);

这里,我们定义的函数被称为模块的插入和删除。只有第一个的插入函数是必要的。目前,它们只是打印消息到内核环缓冲区(可以在用户空间通过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()下添加如下三行:

static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");

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

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

static int __init reverse_init()
{
    if (!buffer_size)
        return -1;
    printk(KERN_INFO
        "reverse device has been registered, buffer size is %lu bytes\n",
        buffer_size);
    return 0;
}

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

导航

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

如果基于终端的工具不是你的最爱,那么就访问http://lxr.free-electrons.com吧。它是一个基于web的内核导航工具,即使它的功能没有Cscope来得多(例如,你不能方便地找到函数的用法),但它仍然提供了足够多的快速查询功能。

现在是时候来编译模块了。你需要你正在运行的内核版本头文件(linux-headers,或者等同的软件包)和build-essential(或者类似的包)。接下来,该创建一个标准的Makefile模板:

obj-m += reverse.o
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

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

$ dmesg | tail -1
[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes

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

混杂设备

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

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

static struct miscdevice reverse_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "reverse",
    .fops = &reverse_fops
};
static int __init reverse_init()
{
    ...
    misc_register(&reverse_misc_device);
    printk(KERN_INFO ...
}

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

static void __exit reverse_exit(void)
{
    misc_deregister(&reverse_misc_device);
    ...
}

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

static struct file_operations reverse_fops = {
    .owner = THIS_MODULE,
    .open = reverse_open,
    ...
    .llseek = noop_llseek
};

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

关闭和打开

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

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

struct buffer {
    char *data, *end, *read_ptr;
    unsigned long size;
};

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

static struct buffer buffer\_alloc(unsigned long size) { struct buffer *buf; buf = kzalloc(sizeof(buf), GFP\_KERNEL); if (unlikely(!buf)) goto out; ... out: return buf; }

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

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

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

有了buffer\_alloc()buffer\_free()openclose方法就变得很简单了。

static int reverse_open(struct inode *inode, struct file *file)
{
    int err = 0;
    file->private_data = buffer_alloc(buffer_size);
    ...
    return err;
}

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\_head*t read*queue字段开始。你也应该包含linux/sched.h头文件。可以使用DECLARE\_WAITQUEUE()宏来静态声明一个等待队列。在我们的情况下,需要动态初始化,因此添加下面这行到buffer\_alloc()

init_waitqueue_head(&buf->read_queue);

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

static ssize_t reverse_read(struct file *file, char __user * out,
        size_t size, loff_t * off)
{
    struct buffer *buf = file->private_data;
    ssize_t result;
    while (buf->read_ptr == buf->end) {
        if (file->f_flags & O_NONBLOCK) {
            result = -EAGAIN;
            goto out;
        }
        if (wait_event_interruptible
        (buf->read_queue, buf->read_ptr != buf->end)) {
            result = -ERESTARTSYS;
            goto out;
        }
    }
...

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

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

    size = min(size, (size_t) (buf->end - buf->read_ptr));
    if (copy_to_user(out, buf->read_ptr, size)) {
        result = -EFAULT;
        goto out;
    }

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

    buf->read_ptr += size;
    result = size;
out:
    return result;
}

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

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

    buf->end = buf->data + size;
    buf->read_ptr = buf->data;
    if (buf->end > buf->data)
        reverse_phrase(buf->data, buf->end - 1);

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

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

    wake_up_interruptible(&buf->read_queue);

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

调试内核代码

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

CFLAGS_reverse.o := -DDEBUG

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

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

惊喜,惊喜!

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

$ make
$ sudo insmod reverse.ko buffer_size=2048
$ lsmod
reverse 2419 0
$ ls -l /dev/reverse
crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse

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

int fd = open("/dev/reverse", O_RDWR);
write(fd, argv[1], strlen(argv[1]));
read(fd, argv[1], strlen(argv[1]));
printf("Read: %s\n", argv[1]);

像这样运行:

$ ./test 'A quick brown fox jumped over the lazy dog'
Read: dog lazy the over jumped fox brown quick A

它工作正常!玩得更逗一点:试试传递单个单词或者单个字母的短语,空的字符串或者是非英语字符串(如果你有这样的键盘布局设置),以及其它任何东西。

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

char *phrase = "A quick brown fox jumped over the lazy dog";
if (fork())
    /* Parent is the writer */
    while (1)
        write(fd, phrase, len);
else
    /* child is the reader */
    while (1) {
        read(fd, buf, len);
        printf("Read: %s\n", buf);
}

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

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
...

这里发生了什么呢?就像举行了一场比赛。我们认为readwrite是原子操作,或者从头到尾一次执行一个指令。然而,内核确实无序并发的,随便就重新调度了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的工作原理相同:

static ssize_t reverse_read(struct file *file, char __user * out,
        size_t size, loff_t * off)
{
    struct buffer *buf = file->private_data;
    ssize_t result;
    if (mutex_lock_interruptible(&buf->lock)) {
        result = -ERESTARTSYS;
        goto out;
}

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

    while (buf->read_ptr == buf->end) {
        mutex_unlock(&buf->lock);
        /* ... wait_event_interruptible() here ... */
        if (mutex_lock_interruptible(&buf->lock)) {
            result = -ERESTARTSYS;
            goto out;
        }
    }

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

    if (copy_to_user(out, buf->read_ptr, size)) {
        result = -EFAULT;
        goto out_unlock;
    }
    ...
out_unlock:
    mutex_unlock(&buf->lock);
out:
    return result;

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

接下来是什么?

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


via: http://www.linuxvoice.com/be-a-kernel-hacker/

译者:GOLinux disylee 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出