Category: Code analysis

Linux kernel学习-进程地址空间

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-process-address-space/ 看完 Linux kernel block I/O 层之后来到进程地址空间管理部分,本文中的很多知识和之前的 [进程基本]、[进程调度]、[内存管理] 等章节的知识相关。 1、基础知识: Linux kernel 给每个进程提供的进程地址空间一般是 32 位或 64 位(硬件相关)的平坦地址空间,但进程是没有权限访问这段地址空间中的所有地址的,能访问的一般是很多的内存地址区间。这种内存地址区间被称为内存区域,进程可以动态添加和删除内存区域到它的地址空间中。内存区域可以有不同的权限,相关进程必须遵守这些权限,例如可读、可写、可执行等。如果进程访问的地址不在一个有效的内存区域中,或者访问时的权限不正确,kernel 将会杀掉进程并给出常见的 "Segmentation Fault" 段错误日志。 内存区域通常包括: 可执行文件的代码段,称为 text 段; 可执行文件的已初始化全局变量段,称为 data 段; 未初始化全局变量段(通常以 0 page 填充),称为 bss 段; 进程的用户空间栈(通常以 0 page 填充); 每个共享库文件的额外 text、data、bss 段,也被装入进程的地址空间; 内存映射文件; 共享内存区域; 匿名内存映射(新版本的 malloc 函数就除了 brk 之外也通过 mmap 实现); 应用程序中的堆 2、内存描述符: kernel 使用 mm_struct 内存描述符结构来表示进程的地址空间信息,它定义在 <linux/mm_types.h> 头文件中,这也是一个非常大的结构。 结构的注释中已经包含比较多的注解了哦。mmap 为地址空间的内存区域(用 vm_area_struct 结构来表示啦,也是上面的代码中)链表,mm_rb 则将其以红黑树的形式进行存储,链表形式方便遍历,红黑树形式方便查找。mm_users 为以原子变量形式保护的使用此地址空间的进程数量值(例如:如果有 4 个线程共享此地址空间,则 mm_users 值为 4),mm_count 为引用计数(所有 mm_users 等于一个引用计数),当 mm_count 值为 0 时表示没有再被使用,可以被释放。total_vm 成员表示所有内存区域的数量。 所有的 mm_struct 结构以链表的形式存在 mm_struct 的 mmlist 成员中,该链表的第一个成员就是 init 进程的 mm_struct :init_mm,该链表被 mmlist_lock 锁保护。 进程的内存描述符是在 task_struct 的 mm 成员中的。fork() 进行创建进程时调用 copy_mm 函数将父进程的内存描述符拷贝给子进程,调用 clone() 函数时如果指定 CLONE_VM 参数将使父进程和子进程地址空间共享(实际上将 mm_users 计数加 1),这种子进程就被称为线程。mm_struct 结构一般是通过 alloc_mm 宏从名为 mm_cachep 的 Slab cache 中分配。 进程退出时调用 exit_mm 函数,该函数再调用 mmput() 函数,此函数中减小地址空间的 mm_users 计数,如果 mm_users 变为 0,调用 mmdrop() 函数减小 mm_count 计数,如果 mm_count 变为 0,则最终调用 free_mm() 宏来释放内存描述符(回归到 Slab cache 中)。 另外需要说明的是 kernel 线程是没有地址空间,也就没有对应的 mm_struct(值为 NULL),kernel 线程使用之前运行的进程的内存描述符,有关 kernel 线程请参考之前的 [进程基本] 文章。 3、VMA 概念: vm_area_struct 结构即内存区域常被称为虚拟内存区域(简写为 VMA),表示的是在一个地址空间中的一个连续内存地址区间,每个内存区域是一个惟一的对象。vm_area_struct...

Linux kernel学习-block层

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-block-layer/ Linux 内核中的 block I/O 层又是非常重要的一个概念,它相对字符设备的实现来说复杂很多,而且在现今应用中,block 层可以说是随处可见,下面分别介绍 kernel block I/O 层的一些知识,你需要对块设备、字符设备的区别清楚,而且对 kernel 基础有一些了解哦。 1、buffer_head 的概念: buffer_head 是 block 层中一个常见的数据结构(当然和下面的 bio 之类的结构相比就差多了哦,HOHO)。 当块设备中的一个块(一般为扇区大小的整数倍,并不超过一个内存 page 的大小)通过读写等方式存放在内存中,一般被称为存在 buffer 中,每个 buffer 和一个块相关联,它就表示在内存中的磁盘块。kernel 因此需要有相关的控制信息来表示块数据,每个块与一个描述符相关联,这个描述符就被称为 buffer head,并用 struct buffer_head 来表示,其定义在 <linux/buffer_head.h> 头文件中。 b_state 字段说明这段 buffer 的状态,它可以是 bh_state_bits 联合(也在上面的代码中,注释说明状态,应该比较好明白哦)中的一个或多个与值。b_count 为 buffer 的引用计数,它通过 get_bh、put_bh 函数进行原子性的增加和减小,需要操作 buffer_head 时调用 get_bh,完成之后调用 put_bh。b_bdev 表示关联的块设备,下面会单独介绍 block_device 结构,b_blocknr 表示在 b_bdev 块设备上 buffer 所关联的块的起始地址。b_page 指向的内存页即为 buffer 所映射的页。b_data 为指向块的指针(在 b_page 中),并且长度为 b_size。 在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的数据结构,它曾经是 kernel 中 I/O 的基本单位(现在已经是 bio 结构),它曾被用于为一个块映射一个页,它被用于描述磁盘块到物理页的映射关系,所有的 block I/O 操作也包含在 buffer_head 中。但是这样也会引起比较大的问题:buffer_head 结构过大(现在已经缩减了很多),用 buffer head 来操作 I/O 数据太复杂,kernel 更喜欢根据 page 来工作(这样性能也更好);另一个问题是一个大的 buffer_head 常被用来描述单独的 buffer,而且 buffer 还很可能比一个页还小,这样就会造成效率低下;第三个问题是 buffer_head 只能描述一个 buffer,这样大块的 I/O 操作常被分散为很多个 buffer_head,这样会增加额外占用的空间。因此 2.6 开始的 kernel (实际 2.5 测试版的 kernel 中已经开始引入)使用 bio 结构直接处理 page 和地址空间,而不是 buffer。 2、bio: 说了一堆 buffer_head 的坏话,现在来看看它的替代者:bio,它倾向于为 I/O 请求提供一个轻量级的表示方法,它定义在 <linux/bio.h> 头文件中。 该定义中已经有详细的注释了哦。bi_sector 为以 512 字节为单位的扇区地址(即使物理设备的扇区大小不是 512 字节,bi_sector 也以 512 字节为单位)。bi_bdev 为关联的块设备。bi_rw 表示为读请求还是写请求。bi_cnt 为引用计数,通过 bio_get、bio_put 宏可以对 bi_cnt 进行增加和减小操作。当 bi_cnt 值为 0 时,bio 结构就被销毁并且后端的内存也被释放。 I/O 向量: bio 结构中最重要的是 bi_vcnt、bi_idx、bi_io_vec...

Linux kernel学习-内存管理

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-management/ 接着之前的 Linux kernel 学习步伐,来到极其重要的内存管理部分,继续本文内容,需要先了解内存寻址的基础知识,见之前的 [内存寻址] 博文。 1、内存页及内存区域: 正如之前所说,Linux kernel 使用物理页作为内存管理的基本单位,其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成,系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页,它的定义在 include/linux/mm_types.h 头文件中: 其中的 flags 用于表示页的状态(是否为脏或者被锁定等),_count 即为页的引用计数,kernel 一般使用 page_count 宏调用 atomic_read 函数原子的读取此值,page_count 返回 0 表示此页可用。如果一个页被作为 page cache 使用,则 page 的 mapping 字段指向映射的 inode 的 address_space 对象,如果页被作为私有数据(作为 buffer_heads 缓冲、buddy 系统等),则 private 常包含对应的信息。注意其中的 virtual 字段为页的虚拟地址,结合之前的知识,对于高端内存来说,其并没有被固定映射到 kernel 地址空间中,因此如果 virtual 字段为 NULL,则表示此页必须被动态映射。 kernel 使用 page 结构记录系统中的所有页,因此 struct page 的大小应该要尽量小以减少内存占用,另外 kernel 必须知道页是否空闲,如果不空闲则拥有者是谁。 由于实际硬件限制,Linux kernel 不可能使用全部的物理内存,kernel 为此将内存划分为不同的区域,一个区域中的内存属性应该也相同。kernel 中常见的内存区域有 ZONE_DMA(可用于 DMA 的页)、ZONE_DMA32(与 ZONE_DMA 类似,但只对 32 位设备可用)、ZONE_NORMAL、ZONE_HIGHMEM(并没有被固定映射的高端内存区域),这些内存区域一般都是硬件相关的,例如在 x86 架构下,ZONE_DMA 的范围为 0MB - 16MB,ZONE_HIGHMEM 为高于 896MB 的物理内存,而在 x86_64 架构下 ZONE_HIGHMEM 则为空。需要注意的是内存的分配不会跨域这些不同的内存区域。内存区域在 kernel 中由 struct zone 结构来表示,其中的 name 字段即为内存区域名称。 2、获取页: 分配和释放内存是 Linux kernel 中极其重要又用的极多的接口。先看看 kernel 提供的直接获取以内存页面为单位的 alloc_pages 函数: struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) 此函数是最基本的用于分配大小为 2^order 并且连续的物理页的函数,其返回分配到的第一个页面的 page 指针。 来看看比较重要的 gfp_t 类型的 gfp_mask 值: gfp_t 实际上就是 unsigned int 类型,gfp_mask 常用于指定行为方式、区域方式、类型等信息。常见的行为方式标志有:__GFP_WAIT(标志分配器可以睡眠,明显不适用于中断上下文中)、__GFP_IO(分配器可以启动磁盘 I/O)等。区域方式指定内存从哪里分配,对应的就有:__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM(从高端内存或普通内存中分配)。类型标志则用于简化分配时的指定操作,常见的有:GFP_ATOMIC(高优先级并不可睡眠,常用于中断、中断下半部、持有自旋锁等环境中)、GFP_NOIO(表示分配可中断但不可以发起 I/O 操作)、GFP_NOFS(分配时不可发起文件 I/O 操作)、GFP_KERNEL(最常见的分配标志,常用于可以睡眠的进程上下文中)、GFP_USER(用于分配内存给用户进程)、GFP_DMA 等。 需要注意的是对 __get_free_pages 和 kmalloc 函数(下面会分别说明)不能指定 __GFP_HIGHMEM 标志,因为它们都是直接返回的虚拟地址,而非 page 结构指针,如果指定了 __GFP_HIGHMEM,则他们可能分配到的内存并没有被映射到 kernel 地址空间,因此这样得不到虚拟地址。只有 alloc_page 函数可以分配高端内存,这个限制在下面的...

Linux kernel percpu变量解析

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-percpu-variable/ Linux 2.6 kernel 中的 percpu 变量是经常用到的东西,因为现在很多计算机都已经支持多处理器了,而且 kernel 默认都会被编译成 SMP 的,相对于原来多个处理器共享数据并进行处理的方式,用 percpu 变量在 SMP、NUMA 等架构下可以提高性能,而且很多情况下必须用 percpu 来对不同的处理器做出数据区分。 本文以 kernel 中的 softirq 为例简单说下 percpu 变量,我们先来看看 kernel 中唤醒 ksoftirqd 的实现,ksoftirqd 在 ps 命令看到的进程列表中很容易找到,是每个处理器都有一个(如果有 4 个处理器,则有 4 个 kernel 线程名称分别从 ksoftirqd/0 到 ksoftirqd/3),关于 softirq 本身的实现不在本文讨论范围内,唤醒 ksoftirqd 的实现在 kernel/softirq.c 文件中: 这里就用到了 percpu 变量 ksoftirqd,它是通过 DEFINE_PER_CPU 宏来进程定义的 percpu task_struct 列表,通过 __get_cpu_var 宏来得到相应处理器的 ksoftirqd/n 的 task_struct,然后调用 wake_up_process 函数唤醒进程(也就是 ksoftirqd/n kernel 线程),关于 wake_up_process 等进程调度的相关实现在之前的日志中有介绍的,请参考 [这里]。 __get_cpu_var、DEFINE_PER_CPU 等 percpu 宏的实现在 include/linux/percpu.h、include/asm-generic/percpu.h 等头文件中。先看看 include/asm-generic/percpu.h 中的一些定义: 通常所有的 percpu 变量是一起存放在特定的 section 里的,像上面头文件中的 .data.percpu 基础 section( 当然非 SMP 系统下就是 .data 了)、.shared_aligned、.first section。使用 objdump 可以看到编译 kernel 时的 vmlinux 文件的 section(结果没有完全显示): 可以看到 vmlinux 文件中的 .data 和 .data.percpu section。 percpu 变量的地址实际上就是其在上面说到的 section 里的偏移量,这个偏移量还要加上特定处理器的偏移量(也就是上面头文件中的 per_cpu_offset、my_cpu_offset 等)得到最终的变量地址,并最终以指针引用的方式得到值,这样访问的效果就有点类似于访问全局变量了。percpu 变量通常用于更新非常频繁而访问机会又相对比较少的场合,这样的处理方式可以避免多处理器环境下的频繁加锁等操作。 从上面的注释也可以看到 per_cpu_offset 是在一个 percpu 变量上增加的偏移量,大多数系统架构下使用 __per_cpu_offset 数组来作为偏移量,而 x86_64 等架构下处理方式则不同。my_cpu_offset 是在调用 per_cpu_offset 时使用 smp_processor_id() 得到当前处理器 ID 作为参数,__my_cpu_offset 则是用 raw_smp_processor_id() 的值作为 per_cpu_offset 的参数(smp_processor_id() 在抢占被关闭时是安全的)。SHIFT_PERCPU_PTR 宏用于给指针增加偏移量,它使用的 RELOC_HIDE 宏在不同的编译器下实现不同,在 include/linux/compiler.h 头文件中,看看 gcc 编译下的处理: 可以看到 gcc 中使用内嵌汇编先将 ptr 值赋给 __ptr(unsigned long 类型),然后在 __ptr 基础上增加偏移量,这样可以避免编译报错,ptr...

Linux kernel kfifo分析

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-kfifo/ kfifo 是 Linux kernel 中的一个通用队列实现,对于 kernel 中常见的 FIFO 队列应用还是很有用的,本文主要简单介绍分析下 Linux kernel kfifo。实际上 ChinaUnix 上有个 kfifo 的分析文章,但已经比较老(基于 Linux 2.6.10),而且我现在用的 2.6.34 版本 kernel 中 kfifo 实现有很多改动,故自己简单写下,ChinaUnix 上的 kfifo 介绍帖子在这里: http://bbs.chinaunix.net/thread-1994832-1-1.html kfifo 定义在 include/linux/kfifo.h 头文件中,我们经常使用的就是 kfifo 结构,看看它的定义: kfifo 也像其它队列那样提供了两个主要操作:入队列(in) 和 出队列(out),对应于上面结构中的 in 和 out 两个偏移量,in 偏移量为下次入队列的位置,out 为下次出队列的位置,很容易也能想到 out 值必须小于等于 in 值,当 out 值等于 in 值时表示队列为空,kfifo 中 buffer 为队列的空间,size 为空间大小,必须为 2 的 k 次幂值(原因在下面说明)。当然如果 in 值等于队列长度了,就表示队列已经满了。 先看看 kfifo 最简单的一些操作实现,在 kernel/kfifo.c 文件中: 调用 kfifo_alloc 可以自动分配空间并初始化,你也可以调用 kfifo_init 函数使用自己的空间来初始化队列,可以看到这两个函数中都用 is_power_of_2 做了检查队列空间的操作。kfifo_free 释放队列,它会调用 _kfifo_init 函数(参数为 NULL 和 0 清空队列),调用 kfifo_reset 可以重置队列(将 in 和 out 都设为 0)。你也可以用 DECLARE_KFIFO 和 INIT_KFIFO 静态定义一个 kfifo 队列,尽管这不太会被用到。 调用 kfifo_in 函数将数据加入队列,kfifo_out 将数据从队列中取出并从队列中删除(增加 out 值),Linux 还提供了 kfifo_out_peek 函数从队列中取数据但并不删除(不增加 out 值)。kfifo_in 中会先调用 __kfifo_in_data 将输入加入队列,然后调用 __kfifo_add_in 增加 in 的值,kfifo_out 相反则调用 __kfifo_out_data 和 __kfifo_add_out 函数取出数据并删除。 kfifo 中同时提供了 kfifo_from_user 函数用户将用户空间的数据加入到队列中,它会先调用 __kfifo_from_user_data 将用户空间的数据加入队列,再调用 __kfifo_add_in 增加 in 的值。看看 __kfifo_from_user_data 的实现: 可以看到 __kfifo_from_user_data 中是直接调用 copy_from_user 将用户空间的数据拷贝到 kfifo 队列的空间中。相应的也有 kfifo_to_user 函数将队列中的数据取出到用户空间的地址,他就调用 copy_to_user 将队列中数据拷贝到用户空间。 需要注意的是 __kfifo_from_user_data 中用到的 __kfifo_off 函数: __kfifo_off 是根据指定的偏移量得到索引值,由这里也可以看出为什么队列的大小为什么必须是 2 的...

Linux kernel学习-内存寻址

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-addressing/ 近日在看 Understanding the Linux kernel(慢慢啃E文原版,以下简称 ULK),这本书虽然已经是第三版了,但它基于的 Linux kernel 版本却不是很新,现在 Linux kernel 都已经出到 3.4 版本了,这本书还是基于 2.6.11 的 kernel,不得不说 Linux kernel 的更迭速度太快了。 下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习,这本书中不对应的地方我会尽量找到新 kernel 中的实现,并尽量自己做个了解,日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。 首先已完成第一章:Introduction(这一章没有 Linux kernel 代码),来到第二章 Memory Addressing,开始是介绍逻辑地址、线性地址、物理地址的对应关系,虽然之前用汇编写过 Linux 的 bootloader,用到过实模式和保护模式,但对 GDT、LDT 的概念并没有深入了解过。这一章开篇就介绍了 Intel 80X86 硬件上内存分段的实现,包括段选择子,段寄存器,段描述符。 1、段式内存管理: 每个内存段由 8 个字节的段描述符来表示段的特征。段描述符被存储在 GDT 或者 LDT 中。内存中 GDT 的地址和大小包含在 gdtr 控制寄存器中,LDT 的地址和大小包含在 ldtr 控制寄存器中。段寄存器的高 13 位为段描述符在 GDT 或者 LDT 中的索引,GDT 或者 LDT 结构中包含基地址、段长度等信息。通过检查指令地址和段长度并确定没有越界以及权限是否正确之后,由于 线性地址 = 段基指 + 偏移地址,GDT 或者 LDT 中的基地址加上指令中的偏移量就可以得到需要的线性地址。 备注:由于每个进程都可以有 LDT,而 GDT 只有一个,为满足需求 Intel 的做法是将 LDT 嵌套在 GDT 表中。 Linux kernel 中的内存分段: Linux中所有进程使用相同的段寄存器值,因此它们的线性地址集也是相同的,不管在用户模式还是内核模式,都可以使用相同的逻辑地址,32位 kernel下为 4G 的地址空间。 ULK 中介绍的 user code、user data、kernel code、kernel data 这四个段对应的段选择子的宏为:__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS,2.6.11 中这4个宏定义在 include/asm-i386/segment.h 头文件中,2.6.34 中已经挪到 arch/x86/include/asm/segment.h 里,因为 2.6.34 中 i386 和 x86_64 的代码已经尽可能的合并到 x86 目录中,而不像老版本的代码那样弄成两个目录。定义如下: 下面是 Linux kernel GDT 的实现: 由于 kernel 中每个内核需要有一个 GDT,因此就有一个 GDT table,ULK 中说的是存在 cpu_gdt_table 中,GDT 的地址和大小存在 cpu_gdt_descr 中,2.6.11 kernel 里都是放在 arch/i386/kernel/head.S,使用的地方: 到了 2.6.34 中已经改为: 可以看到 2.6.34 中去掉了原来的 cpu_gdt_table 变量(详见 kernel commit bf50467204b435421d8de33ad080fa46c6f3d50b),新增了一个 gdt_page 结构存放...

page cache诊断控制工具 vmtouch 源代码分析

本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/vmtouch-code-analysis/ vmtouch 是一个 portable 的 page cache 诊断和控制工具,可以查看文件或者设备中有多少在 page cache 中,知道之后对这些在 page cache 中的内存引用可以避免 page fault,支持将文件的内容从 page cache 逐出,同时还可以将文件手工 touch 到 page cache 中,支持 lock 文件部分区域到 memory 中防止被交换出去从而提高。 vmtouch 可以在 Linux、BSD 等系统上使用,在这下载编译: http://hoytech.com/vmtouch/ 今天简单看了下 vmtouch 的代码,发现还比较简单,自己写个类似的程序验证之后,将代码分析结果写下。vmtouch 的代码比较少,我只贴出最关键的一个函数 vmtouch_file(关键部分已经高亮显示),这个函数做 分析 page cache 使用、touch、lock 的操作,其它部分只是加了读了目录的遍历处理之类的。 稍微有点基础就可以看明白了,先 mmap 映射文件到当前进程,按 page size 对齐之后,调用 mincore 函数就可以得到文件中每一个 page 是否在 page cache 中,结果保存在 mincore_array 数组中,该数据中每个字节的第一位即表示是否在 page cache 中。 将文件内容逐出(指定 o_evict)出 page cache 是通过 posix_fadvise 函数调用 fadvise 系统调用来实现的(BSD通过 msync 实现,这个在 Linux 上没有效果)。fadvise 系统调用可以告诉 kernel 要操作的文件在接下来要干什么,kernel 可以提前做一些操作而提高性能,Linux kernel 里实现了以下几种控制方式: POSIX_FADV_NORMAL - 正常操作,对文件使用底层设备的默认 readahead 值; POSIX_FADV_SEQUENTIAL - 顺序I/O,对文件使用两倍的 readahead 值; POSIX_FADV_RANDOM - 随机I/O,禁用文件上的 readahead; POSIX_FADV_NOREUSE - 只使用一次 POSIX_FADV_WILLNEED - 很快需要使用,对文件使用非阻塞读到 page cache POSIX_FADV_DONTNEED - 不再需要使用文件,从 page cache 中逐出 posix_fadvise 加 POSIX_FADV_DONTNEED 参数就可以将文件从 page cache 中逐出,需要注意的是如果需要确保文件从 page cache 中逐出,还需要在调用 fadvise 之前用 fsync/fdatasync/sync_file_range 之类的函数将 dirty page 清理。 下面是我在 Linux 下用 posix_fadvise 的一个测试程序测试的结果: 从 free 命令的结果可以很明显的看到,dd 之后基本文件都在 page cache 中,fadvise 之后从 page cache 中正确逐出。 接着是 vmtouch 中的 touch 操作(指定 o_touch)就更简单了,对 mmap 到的地址直接遍历引用,不在 page...

Thunderbird(Firefox)病毒扫描原理分析及禁用方法

本文博客链接:https://zohead.com/archives/thunderbird-virus-scan-analyse-disable/ 笔者现在浏览网页和电子邮件全部都用 Mozilla 系的 Firefox 和 Thunderbird 了,这俩软件算是我的 Windows 系统中启动之后运行时间最长的两个应用程序,都非常好用,Thunderbird 虽然没有 Firefox 那么受欢迎,也算比 Outlook 好用很多(仅仅个人意见 ^_^),同时都支持扩展和附加组件。 最近发现有一点需要吐槽的是 Thunderbird 和 Firefox 默认下载附件或者下载东西完成之后都需要运行杀毒软件进行病毒扫描,而且附件比较大时还相当缓慢,对于我这种不想装杀毒软件的人或者受不了速度影响的人会有点感官影响,HOHO,寻办法禁用之。 1、Firefox 先说说比较简单的 Firefox 下禁用病毒扫描的方法(可以直接 百度、GG 到): 地址栏中输入“about:config”,忽略坑爹的 “保修提示”,在过滤器中中输入“browser.download.manager.scanWhenDone”,通常找到的第一个就是要改的配置,双击将值改为 false,随便下个软件,马上就可以看到效果。 2、老版本 Thunderbird 接下来 Thunderbird 的禁用方法就没有直接的了,老版本的 Thunderbird 似乎也可以在 “about:config” 设置同样的值,但新版本的 “about:config” 中已经没有这个配置,注意 Thunderbird 的 ”about:config“ 不是直接输入了,需要打开首选项,进入 “高级” 选项卡,点 ”常规“,然后打开 ”配置编辑器“ 就是想要的效果。 如果直接要新版本的修改方法,直接忽略分析部分直接跳到最后。 3、Thunderbird 扫描原理分析 无奈新版本的 Thunderbird 在搜索了 N 次之后找不到解决办法,祭出查源代码大法(一般都比较好使,哈哈),不想下那么大的源码包,直接在 Mozilla 官方网站上找 Thunderbird 的在线版本库,找到了 Mozilla Cross-Reference 做参考: http://mxr.mozilla.org/ 进入 Comm. Central 仓库: http://mxr.mozilla.org/comm-central/ 搜索 virus scan 之类的就能看到下面几个代码段,稍加分析,大略知道 Thunderbird 和 Firefox 的病毒扫描原理,简单分析下。 首先在 nsDownloadScanner.cpp 下找到扫描的初始化部分:   下面是同一文件中的比较重要的 IsAESAvailable 的实现:   对是否支持扫描的判断很简单,判断 IAttachmentExecute COM组件是否可用,此组件在 Windows XP SP2 之后的系统中就自带,另外如果在更老的系统中则使用 IOfficeAntiVirus COM组件。 下面是下载管理器 nsDownloadManager.cpp 的处理:   可以看到第12行处会启动 nsDownloadScanner 进行下载附件的扫描处理。 4、组策略处理方法(不完美) 经过粗略了解之后便是解决方法咯,首先看能否对 Thunderbird 禁用 IAttachmentExecute COM组件。 有关 IAttachmentExecute COM组件的配置见这篇文章通过组策略设置“附件管理器”,可以参考这篇文章对整个系统的附件扫描机制做设置: http://edu.kafan.cn/html/hips/11386.html 简单流程: 开始 - 运行,输入:gpedit.msc,打开组策略编辑器,定位到 用户配置 - 管理模板 - Windows组件 - 附件管理器,截图如下,看下应该就能明白 可以看到这个设置是全局,是一种解决方法,但会对其它需要扫描的程序(例如:Outlook)也会造成影响。 5、完美禁用 Thunderbird 病毒扫描 再经过搜寻源代码之后终于找到只改变 Thunderbird 扫描病毒配置的方法,原来 Thunderbird 可能出于安全的考虑将 ”browser.download.manager.scanWhenDone“ 配置从 ”about:config“ 配置编辑器中移除掉了,但这个配置功能并没有真正去掉,我们还有直接修改 Thunderbird 安装目录的配置脚本的方法,进入 Thunderbird 安装目录的 defaults\pref 下,打开 channel-prefs.js 文件编辑,增加扫描的配置。 例如我的配置脚本的路径为:C:\Program Files\Mozilla Thunderbird\defaults\pref\channel-prefs.js,打开该文件,增加一行: pref("browser.download.manager.scanWhenDone", false);...