Linux编程 根据用户的jvm 指定内存大小的大小从内存空间中分配内存,再向这片内存中写入一段你自定的文字

内存教程-动态脚本编程_
优质网站模板分配内存 - Linux设备驱动程序学习笔记_Linux编程_Linux公社-Linux系统门户网站
你好,游客
Linux设备驱动程序学习笔记
来源:Linux社区&
作者:en_wang
kmalloc函数
#include&linux/slab.h&void *kmalloc(size_t size,int flags);
1.不会对所申请的内存清零,保留原有数据
2.参数:size:分配大小
&&&&&&&&&&&&&&& flags:kmalloc行为
3.flags:GFP_KERNEL :内核内存通常的分配方法,可能引起休眠
&&&&&&&&&&&&&&&&&GFP_ATOMIC :用于在中断处理例程或其它运行于进程上下文之外的代码中分配内存,不会休眠
&&&&&&&&&&&&&&&&&GFP_USER:用于为用户空间分配内存,可能会引起休眠
&&&&&&&&&&&&&&&& GFP_HIGHUSER:类似于GFP_USER,不过如果有高端内存的话就从那里分配
&&&&&&&&&&&&&&&& GFP_NOIO:在GFP_KERNEL的基础上,禁止任何I/O的初始化
&&&&&&&&&&&&&&&& GFG_NOFS:在GFP_KERNEL的基础上,不允许执行任何文件系统的调用
&&&&&&&&&&&&&&&& 另外有一些分配标志与上述“或”起来使用
&&&&&&&&&&&&&&&& __GFP_DMA:
&&&&&&&&&&&&&&&& __GFP_HIGHMEM:
&&&&&&&&&&&&&&&& __GFP_COLD:
&&&&&&&&&&&&&&&& __GFP_NOWARN:
&&&&&&&&&&&&&&&& __GFP_HIGH:
&&&&&&&&&&&&&&&& __GFP_REPEAT:
&&&&&&&&&&&&&&&& __GFP_NOFAIL:
&&&&&&&&&&&&&&&& __GFP_NORETRY:
&4.内存区段
linux通常把内存分成三个区段:
可用于DMA内存
存在于特别的地址范围
32位平台为访问(相对)大量内存而存在的一种机制
linux处理内存分配:创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。
&内核只能分配一些预定义的,固定大小的字节数组。若申请任意数量的内存,则得到的可能会多一些,最多可以得到申请数量的两倍。
32或者64,取决于当前体系
128kb,使用的页面的大小&&&&&&&&&&&&
后备高速缓存
驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookaside cache)。 设备驱动程序通常不会涉及后备高速缓存,但是也有例外:在 Linux 2.6 中 USB 和 SCSI 驱动。Linux 内核的高速缓存管理器有时称为“slab 分配器”,相关函数和类型在 &linux/slab.h& 中声明。slab 分配器实现的高速缓存具有 kmem_cache_t 类型。
<SPAN style="COLOR: #.实现过程如下:
kmem_cache_t *kmem_cache_create(const char *name,&size_t size,size_t offset,&                 unsigned long flags,      &void (*constructor)(void *, kmem_cache_t *,unsigned long flags),
      void (*destructor)(void *, kmem_cache_t *, unsigned long flags));/*创建一个可以容纳任意数目内存区域的、大小都相同的高速缓存对象,这些区域的大小都相同,由size参数决定。*/
参数*name: 一个指向&name 的指针,name和这个后备高速缓存相关联,功能是管理信息以便追踪问题;通常设置为被缓存的结构类型的名字,不能包含空格。
参数size:每个内存区域的大小。
参数offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。
参数flags:控制分配方式的位掩码:
SLAB_NO_REAP&&&&&&& 保护缓存在系统查找内存时不被削减,不推荐。
SLAB_HWCACHE_ALIGN& 所有数据对象跟高速缓存行对齐,平台依赖,可能浪费内存。
SLAB_CACHE_DMA&&&&& 每个数据对象在 DMA 内存区段分配.。
2.一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc从中分配内存对象:
viod *kmem_cache_alloc(kmem_cache_t *cache,int flags);
cache是之前创建的高速缓存,flags和传递给kmalloc的相同,并且当需要分配更多的内存来满足kmem_cache_alloc时,高速缓存还会利用这个参数
3.释放一个内存对象,使用kmem_cache_free:
void kmem_cache_free(kmem_cache_t *cache,const void *obj);
4.如果驱动程序代码中和高速缓存有关的部分已经处理完了(典型情况:模块被卸载的时候),这时驱动程序应该释放它的高速缓存:
int kmem_cache_destroy(kmem_cache_t *cache);
/*只在从这个缓存中分配的所有的对象都已返时才成功。因此,应检查 kmem_cache_destroy 的返回值:失败指示模块存在内存泄漏*/
为了确保在内存分配不允许失败情况下成功分配内存,内核提供了称为内存池( "mempool" )的抽象,它其实是某种后备高速缓存。它为了紧急情况下的使用,尽力一直保持空闲内存。所以使用时必须注意: mempool 会分配一些内存块,使???空闲而不真正使用,所以容易消耗大量内存 。而且不要使用 mempool 处理可能失败的分配。应避免在驱动代码中使用 mempool。
内存池的类型为 mempool_t ,在 &linux/mempool.h& ,使用方法如下:
1.创建mempool
mempool_t *mempool_create(int min_nr,
&&&&&&&&&&&&&&&&&&&&&&&&& &mempool_alloc_t *alloc_fn,&&&&&&&&&&&&&&&&&&&&&&&&&& mempool_free_t *free_fn,&&&&&&&&&&&&&&&&&&&&&&&&&& void *pool_data);
/*min_nr 参数是内存池应当一直保留的最小数量的分配对象*/
/*实际的分配和释放对象由 alloc_fn 和 free_fn 处理,原型:*/typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);typedef void (mempool_free_t)(void *element, void *pool_data);/*给 mempool_create 最后的参数 *pool_data 被传递给 alloc_fn 和 free_fn */
你可编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器为你处理这个任务:mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用 kmem_cache_alloc 和 kmem_cache_free&处理内存的分配和释放。
典型的设置内存池的代码如下:
cache = kmem_cache_create(. . .); pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache);
(2)创建内存池后,分配和释放对象:
void *mempool_alloc(mempool_t *pool, int gfp_mask);void mempool_free(void *element, mempool_t *pool);&在创建mempool时,分配函数将被调用多次来创建预先分配的对象。因此,对 mempool_alloc 的调用是试图用分配函数请求额外的对象,如果失败,则返回预先分配的对象(如果存在)。用 mempool_free 释放对象时,若预分配的对象数目小于最小量,就将它保留在池中,否则将它返回给系统。
可用一下函数重定义mempool预分配对象的数量:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);/*若成功,内存池至少有 new_min_nr 个对象*/&
(3)若不再需要内存池,则返回给系统:
void mempool_destroy(mempool_t *pool); /*在销毁 mempool 之前,必须返回所有分配的对象,否则会产生 oops*/
get_free_page与相关函数
1.如果模块需要分配大块的内存,使用面向页的分配技术会更好一些,就是整页的分配。
__get_free_page(unsigned int flags); /*返回一个指向新页的指针, 未清零该页*/get_zeroed_page(unsigned int flags); /*类似于__get_free_page,但用零填充该页*/__get_free_pages(unsigned int flags, unsigned int order); /*分配是若干(物理连续的)页面并返回指向该内存区域的第一个字节的指针,该内存区域未清零*//*参数flags 与 kmalloc 的用法相同;参数order 是请求或释放的页数以 2 为底的对数。若其值过大(没有这么大的连续区可用), 则分配失败*/
2.get_order 函数可以用来从一个整数参数 size(必须是 2 的幂) 中提取 order,函数也很简单:
/* Pure 2^n version of get_order */static __inline__ __attribute_const__ int get_order(unsigned long size){&&&&int order;&&&&size = (size - 1) && (PAGE_SHIFT - 1);&&&&order = -1;&&&&do {&&&&&&&&size &&= 1;&&&&&&&&order++;&&&&} while (size);&&&&return order;}
3.通过/proc/buddyinfo 可以知道系统中每个内存区段上的每个 order 下可获得的数据块数目。
4.。当程序不需要页面时,它可用下列函数之一来释放它们。
void free_page(unsigned long addr);void free_pages(unsigned long addr, unsigned long order);
它们的关系是:
#define __get_free_page(gfp_mask) \&&&&&&&&__get_free_pages((gfp_mask),0)若试图释放和你分配的数目不等的页面,会破坏内存映射关系,系统会出错。
alloc_pages 接口
1.struct page 是一个描述一个内存页的内部内核结构,定义在&linux/Mm_types.h&
Linux 页分配器的核心是称为 alloc_pages_node 的函数:
struct page *alloc_pages_node(int nid, unsigned int flags,&unsigned int order);/*以下是这个函数的 2 个变体(是简单的宏):*/struct page *alloc_pages(unsigned int flags, unsigned int order);struct page *alloc_page(unsigned int flags);/*他们的关系是:*/#define alloc_pages(gfp_mask, order) \&&&&&&&&alloc_pages_node(numa_node_id(), gfp_mask, order)#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
参数nid 是要分配内存的 NUMA 节点 ID,参数flags 是 GFP_ 分配标志, 参数order 是分配内存的大小. 返回值是一个指向第一个(可能返回多个页)page结构的指针, 失败时返回NULL。
alloc_pages 通过在当前 NUMA 节点分配内存( 它使用 numa_node_id 的返回值作为 nid 参数调用 alloc_pages_node)简化了alloc_pages_node调用。alloc_pages 省略了 order 参数而只分配单个页面。
释放分配的页:
void __free_page(struct page *page);void __free_pages(struct page *page, unsigned int order);void free_hot_page(struct page *page);void free_cold_page(struct page *page);/*若知道某个页中的内容是否驻留在处理器高速缓存中,可以使用 free_hot_page (对于驻留在缓存中的页) 或 free_cold_page(对于没有驻留在缓存中的页) 通知内核,帮助分配器优化内存使用*/
vmalloc 和 ioremap
vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:
#include &linux/vmalloc.h& void *vmalloc(unsigned long size);void vfree(void * addr);void *ioremap(unsigned long offset, unsigned long size);void iounmap(void * addr);
kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。
而vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END 的范围中,定义在 &asm/patable.h& 中。vmalloc 分配的地址只在处理器的 MMU 之上才有意义。当驱动需要真正的物理地址时,就不能使用 vmalloc。 调用 vmalloc 的正确场合是分配一个大的、只存在于软件中的、用于缓存的内存区域时。注意:vamlloc 比 __get_free_pages 要更多开销,因为它必须即获取内存又建立页表。因此, 调用 vmalloc 来分配仅仅一页是不值得的。vmalloc 的一个小的缺点在于它无法在原子上下文中使用。因为它内部使用 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因此可能休眠。
ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。
ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。
per-CPU变量
per-CPU 变量是一个有趣的 2.6 内核特性,定义在 &linux/percpu.h& 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能
在编译时间创建一个per-CPU变量使用如下宏定义:
DEFINE_PER_CPU(type, name);/*若变量( name)是一个数组,则必须包含类型的维数信息,例如一个有 3 个整数的per-CPU 数组创建如下: */DEFINE_PER_CPU(int[3], my_percpu_array);
虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。 对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:
get_cpu_var(sockets_in_use)++;put_cpu_var(sockets_in_use);
当要访问另一个处理器的变量副本时, 使用:
per_cpu(variable, int cpu_id);
当代码涉及到多处理器的per-CPU变量,就必须实现一个加锁机制来保证访问安全。
动态分配per-CPU变量方法如下:
void *alloc_percpu(type);void *__alloc_percpu(size_t size, size_t align);/*需要一个特定对齐的情况下调用*/void free_percpu(void *per_cpu_var); /* 将per-CPU 变量返回给系统*//*访问动态分配的per-CPU变量通过 per_cpu_ptr 来完成,这个宏返回一个指向给定 cpu_id 版本的per_cpu_var变量的指针。若操作当前处理器版本的per-CPU变量,必须保证不能被切换出那个处理器:*/per_cpu_ptr(void *per_cpu_var, int cpu_id);/*通常使用 get_cpu 来阻止在使用per-CPU变量时被抢占,典型代码如下:*/int cpu; cpu = get_cpu()ptr = per_cpu_ptr(per_cpu_var, cpu);/* work with ptr */put_cpu();/*当使用编译时的per-CPU 变量, get_cpu_var 和 put_cpu_var 宏将处理这些细节。动态per-CPU变量需要更明确的保护*/
per-CPU变量可以导出给模块, 但必须使用一个特殊的宏版本:
EXPORT_PER_CPU_SYMBOL(per_cpu_var);EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
/*要在模块中访问这样一个变量,声明如下:*/DECLARE_PER_CPU(type, name);
注意:在某些体系架构上,per-CPU变量的使用是受地址空间有限的。若在代码中创建per-CPU变量, 应当尽量保持变量较小.
获得大的缓冲区大量连续内存缓冲的分配是容易失败的。到目前止执行大 I/O 操作的最好方法是通过离散/聚集操作 。
在引导时获得专用缓冲区
若真的需要大块连续的内存作缓冲区,最好的方法是在引导时来请求内存来分配。在引导时分配是获得大量连续内存页(避开 __get_free_pages 对缓冲大小和固定颗粒双重限制)的唯一方法。一个模块无法在引导时分配内存,只有直接连接到内核的驱动才可以。 而且这对普通用户不是一个灵活的选择,因为这个机制只对连接到内核映象中的代码才可用。要安装或替换使用这种分配方法的设备驱动,只能通过重新编译内核并且重启计算机。
当内核被引导, 它可以访问系统种所有可用物理内存,接着通过调用子系统的初始化函数, 允许初始化代码通过减少留给常规系统操作使用的 RAM 数量来分配私有内存缓冲给自己。
在引导时获得专用缓冲区要通过调用下面函数进行:
#include &linux/bootmem.h&/*分配不在页面边界上对齐的内存区*/void *alloc_bootmem(unsigned long size); void *alloc_bootmem_low(unsigned long size); /*分配非高端内存。希望分配到用于DMA操作的内存可能需要,因为高端内存不总是支持DMA*//*分配整个页*/void *alloc_bootmem_pages(unsigned long size); void *alloc_bootmem_low_pages(unsigned long size);/*分配非高端内存*//*很少在启动时释放分配的内存,但肯定不能在之后取回它。注意:以这个方式释放的部分页不返回给系统*/void free_bootmem(unsigned long addr, unsigned long size); 9
【内容导航】
相关资讯 & & &
& (01月25日)
& (01月14日)
& (02月09日)
& (01月15日)
& (01月14日)
   同意评论声明
   发表
尊重网上道德,遵守中华人民共和国的各项有关法律法规
承担一切因您的行为而直接或间接导致的民事或刑事法律责任
本站管理人员有权保留或删除其管辖留言中的任意内容
本站有权在网站内转载或引用您的评论
参与本评论即表明您已经阅读并接受上述条款Linux就这个范儿 第15章 七种武器
linux 同步IO: sync、fsync与fdatasync
Linux中的内存大页面huge page/large page
David Cutler
Linux读写内存数据的三种方式
Linux就这个范儿 第15章 七种武器 &linux 同步IO: sync、fsync与fdatasync & Linux中的内存大页面huge page/large page &David Cutler &Linux读写内存数据的三种方式
台湾作家林清玄在接受记者采访的时候,如此评价自己30多年写作生涯:&第一个十年我才华横溢,&贼光闪现&,令周边黯然失色;第二个十年,我终于&宝光现形&,不再去抢风头,反而与身边的美丽相得益彰;进入第三个十年,繁华落尽见真醇,我进入了&醇光初现&的阶段,真正体味到了境界之美&。长夜有穷,真水无香。领略过了Linux&身在江湖&的那种惊心动魄以及它那防御系统的繁花似锦,该是回过头来体味性能境界之美的时候了。毕竟仅能经得起敲打还是不能独步武林的!《七种武器》作为古龙小说的代表作之一,共分为七个系列:长生剑、离别钩、孔雀翎、碧玉刀、多情环、霸王枪、拳头七种非一般江湖武器,件件精美绝伦。七种令人闻风丧胆、不可思议的武器,七段完全独立的故事,令人叹为观止,不能掩卷。恰巧Linux也拥有七种武器,分别是:fork、VFS、mmap、epoll、udev、LVS、module,同样也是七种非一般的&江湖武器&,件件精美绝伦。只不过它们是七种令人肃然起敬、不可怠慢的武器。七种武器看似完全不相干但内部却有着千丝万缕的关联,实在是令人叹为观止,不得不悉心研究一番。本章将给你逐一地展示Linux历拥有的七种武器。但是,请你不要忘记,古龙先生的《七种武器》表面上写的是杀戮,实际上写的是人性:笑、相聚、自信心、诚实、仇恨、勇气、不放弃。那么Linux上的七种武器应该怎样去看呢?我想,答案在每一个人的心中。
长生剑:fork古龙的《长生剑》不是写长生剑的主人白玉京,而是写弱女子袁紫霞。她一个人来清理门户,大大小小的武林高手被她轻轻松松地置之死地。小说上说:&一个人只要懂得利用自己的长处,根本就不必用武功也一样能够将人击倒。&她的长处是笑&&无论多么锋利的剑,也比不上那动人的一笑。长生剑这个形式代表的是一种任何时候处变不惊、泰然自若的淡定。深处逆境甚至是面临死亡都能从容一笑化解愁怀。这正好对应了Linux的fork,因为fork之后就会产生一个新的进程,可以使你的程序异常稳定。为什么会这样呢?故事得从这个地方说起&&
15.1.1从线程说起话说1969年在Unix诞生之曰起,它就足以多任务而著称的,且一直独领风骚几十年。然而,从这一天起,一个无休止的争论也就开始了,那就是怎么实现多任务。观点很多,但普遍存在的主要有两种:一种是以进程为主的强调任务独立性的观点;另一种是以线程为主的强调任务协同性的观点。这两种观点争来争去到现在都没有一个结论,但这是一种极具价值的争论。看世间事,总是此起彼伏,变幻莫测。之前,一直是以进程为代表的Unix这一派占优。但是就在人们为争论得不可开交而乐此不疲之际,以线程为代表的Windows横空出世了,并迅速占领了个人电脑这块辽阔的疆土,从此线程这一派占据了绝对的优势。十分清楚地告诉人们,多任务只有这么玩才深入人心。
进程和线程的差异那么到底什么是进程,什么又是线程呢?这个问题基本上已经快被所有面试官问烂了,很多人也在这个地方折了N多回。从现代的意义上来看,进程就像一个大容器。在程序被运行之后,就相当于将程序装进了这个容器,然后你还可以往容器里面加其他东西,比如一些共享库。当程序被运行两次时,容器里的东西并不会被倒掉,而是再找一个新的容器来装程序。 多个程序实例那么线程呢?普遍意义上来说,线程也属于进程的一部分,这相当于在进程这个容器中又划分出了许多的小隔间。这些小隔间将迸程这个大容器中的程序划分成了很多独立的部分。但是这些小隔间并没有将整个程序划分干净,还留下了一些,而留下的这些可以在任意的小隔间中游荡。而且往大容器里添加的东西也不会特意分给哪个小隔间,甚至还能添加新的小隔间。游离于小隔间之外的东西很多时候是个很麻烦的角色,因为会有好多小隔间会同时需要它们,这就必须得制定一些规矩来协调或商量着来,否则就会出乱子,把整个程序弄得四分五裂。协调或商量的方法由操作系统来提供,用纯技术的说法就是线程同步机制。作为一个合格的操作系统,一般都会提供:互斥锁、条件变量、信号量等机制,或者相似的机制。虽然方法有了,但是如何商量、协调什么这种需要智商的问题,就只能留给程序员们了。对于进程这个大容器,如果你不是很愤青非得要将这个铁饭碗戳一个洞的话,也就没啥可要协调或商量的了。但是人在社会上混,跟其他人老死不相往来的话,估计也混不多久就被人打冷宫了。而且你也不可能一个人什么都会做,所以必须寻求帮助。要寻求帮助就得与人沟通。放在进程这个大容器里的程序也一样,不可能什么都能做得来,也需要寻求其他程序的帮助,和它们沟通。程序与程序之间的沟通方法也由操作系统来提供,用纯技术的说法孰是进程通信机制(IPC进程间通信)。那么作为一个合格的操作系统,一般都会提供:信号、管道、I/O重定向、套接字等机制,或者类似的机制。同样的,虽然方法有了,沟通什么内容以及沟通的技巧等这些需要智商的事儿,还得由程序员自己来处理。在操作系统层面,对进程和线程的支持是完全不同的,当然从目前来看没有谁敢不同时支持它们。以Windows为代表支持线程这一派的操作系统,往往在系统内部是以线程为调度实体的,进程对于这类操作系统来说就是一堆数据结构罢了:以Unix为代表的支持进程这一派的操作系统,其系统内部的调度实体是进程,而线程基本上都是在耍花招了。也正因为这样,由于Windows将进程这个大容器基本上给忽略掉了,而每个线程又那么小巧精悍,调度起来远比Unix舞动那些大容器要轻松许多,从多任务的调度效率上Windows占有了很大的优势。这就相当于Windows耍的是小李飞刀,而Unix们抡的可是流星锤,谁更轻快灵活一看便明。但是谁才会是最终的赢家,这个还真不好说。
2. Linux的线程方案有竞争才有动力。看到Windows的大红大紫,Unix们也不能甘为人下,自然也要在线程方面有所斩获。但是Unix本身并不具备对线程的支持,完全照搬Windows的设计又心有不甘,于是很多方案被提出来了。对于Linux,恐怕最有名的要数LinuxThreads方案了。在开始时,Linux是完全的Unix克隆,在内核中并不支持线程。但是它的确可以通过clone()系统调用将进程作为调度的实体。这个调用创建了调用进程的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads万案使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这个方案有太多缺点,让Windows总是有一种&一直被追赶从未被超越&的自豪。首先,LinuxThreads有一个非常有名的设计就是管理线程,它要解决下面这些问题:(1)响应终止信号并杀死整个进程;(2)在线程执行完之后回收以堆栈形式使用的内存;(3)等待终止的线程,防IE它们进入僵尸状态;(4)回收线程本地数据;(5)防止主线程过早退出或在所有线程都退出之后唤醒进入睡眠状态的主线程。但是有这样的一个线程存在,就等于增加了整个程序的额外开销(创建、销毁、上下文切换等)。而且管理线程只能在一个CPU上运行,显然不适合现在的多核CPU。其次,由于线程都是使用进程来模拟的,那么每个线程都会使用一个不同的进程ID,这与我们所理解的线程属于进程这个概念有强烈的冲突。而且还会导致/proc目录中充满了的进程项,而它们实际上又可能只是线程。最后,这些问题还都只是冰山一角,更为复杂的问题我也没有兴趣去讨论,因为LinuxThreads只是记忆了。现代的Linux已经不是那个Linux了,这个时候的Windows就真的是瘟到死了。因力Linux有了NPTL。NPTL的全称是Native POSIX Thread Library,翻译过来就是原生POSIX线程库。NTPL可以让Linux内核高效地运行那些使用POSIX风格的线程API所编写的程序。我在一个很老的32位系统( P3 800MHZ)下做了一个实验,成功地同时跑了10万个线程,启动这些线程只用了不到2秒。作为对比,在不支持NPTL的内核上,我的这个测试花了大约15分钟。在这个测试中NPTL所表现出来的性能提升让我惊诧不已。NPTL是一种1:1的线程方案,一个线程会与内核的一个调度实体一一对应,线程的创建和回收都由内核负责,这样就可以规避掉LinuxThreads的一切问题。这是一种最简单的合理线程实现方案。但是业界还有另外一个备选方案,就是m:n方案。这种方案中用户线程要多于调度实体。如果NPTL选择以这种方式实现的话,会使得线程上下文切换更快,因为它避免了系统调用。但是m:n的方案是以系统复杂度为代价的。既然Linux的骨子里有些&笨&,复杂性的东西是搞不来的,所以NPTL采用的依然是1:1的线程方案。NPTL方案还引入了新的线程同步机制&&futex。futex是fast userspace mutex的缩写,翻译过来就是快速用户空间互斥体。也就是说,这个互斥锁是完全在用户空间中实现的,这就规避了由用户空间到内核空间切换的代价,使得这个锁的效率更高。当然,futex只是Linux的一种底层机制,程序员要面对的那些锁并没有什么形式上的变化,因为它们是对futex的封装。NPTL方案的引入并没有给程序员带来什么负担,或者说它对程序员是透明的。因为原有的LinuxThreads的API在NPTL万案中并没有改变,而且还更加符合POSIX标准的规定。这也是本书不打算深入介绍NPTL的根本原因,因为什么都没有变,还说它做什么呢?NPTL是在Linux 2.6内核开始引入的。一个比较有趣的地方是,Linux内核本身的多任务调度实体被称为&内核线程&。而且经常有人会非常兴奋的说,Linux已经跟Windows 一样了,是以线程为调度实体的。的确不假,从2.6开始,线程是Linux原生支持的特性了,但是与Windows还是有很大差别的。首先,Windows的调度实体就是线程,进程只是一堆数据结构。而Linux不是。Linux将进程和线程做了同等对待,进程和线程在内核一级没有差别,只是通过特殊的内存映射方法使得它们从用户的角度看来有了进程和线程的差别。其次,Windows至今也没有真正的多进程概念,创建进程的开销远大于创建线程的开销。Linux则不然。Linux在内核一级并不区分进程和线程,这使得创建进程的开销与创建线程的开销差不多。最后,Windows与Linux的任务调度策略也不尽相同。Windows会随着线程越来越多而变得越来越慢,这也是为什么Windows服务器在运行一段时间后必须萤启的原因。当然,如果你是微软的VIP客户还是有办法规避这个问题的,但是大部分用户都不是微软的VIP。反观Linux却可以持续运行很长时间,有些Linux服务器已经连续运行了几年,系统的效率也没有什么变化。而且Linux也没有VIP用户的说法,或者人人都可以是Linux的VIP。不管怎样,自从Linux引入了NTPL机制之后,才真正成为一个可以傲视群雄的操作系统,成为一个真正意义的现代操作系统。当然,即便这样也不建议你盲从Linux,因为还有很多特性是Linux所不具备的,这也是Linux之所以没有占领整个天下的根本原因。至于哪些特性是Linux的软肋,我并不想多说,毕竟我还是Linux的死忠。
/MYSQLZOUQI/p/4233630.html
GNU_LIBPTHREAD_VERSION 宏大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:
$ getconf GNU_LIBPTHREAD_VERSION
这会产生类似于下面的输出结果:
linuxthreads-0.10
15.1.2古老而充满活力的进程有关线程方面知识还有很多,但是本书的目的不在于此。现在我们开始回到正题,说说fork了。fork在英语中有&分叉&的含义。在Unix世界里是任务分叉的含义,一个任务分成了两个任务,它也是Unix系统的一个系统调用。我们前面将进程比喻为一个个大容器,那么当引入了fork之后,这个容器就像有了生命,变成了可以分裂的细胞,可以一代一代的繁衍下去。那么将fork比喻为长生剑就一点都不为过。因为自从Unix诞生就拥有这个fork系统调用。如今历经了30多年的不断演进,这个系统调用依旧活力四射,而被众多类Unix操作系统所保留,这其中就包括Linux。屹立30多年而不倒,在这技术日新月异的计算机产业之中,谁能不说它长生?原本死气沉沉的犹如大容器般的进程,引入了fork之后立即拥有了&生命&,怎么能不说它是生的源泉?而如今的多进程系统看似陈旧,却依旧特点鲜明而性能突出,就像一把利剑一样协助程序员们披荆斩棘、开山扩路。进程显然已成为这一类操作系统所固有的优良本质,这又与古龙笔下的《长生剑》所暗喻的&笑&切合的是那样天衣无缝。Fork&就是Linux的饫生剑。
进程的特点不管怎么说,进程都是现代操作系统的一个最基本的概念。如果用比较学术的话语来描述进程,那么应该是:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序代码,还包括当前的活动,通过程序计数器的值和CPU寄存器的内容来表示。这种学术话语往往是很难让我们这群小老百姓看懂的,那么按照我的理解,进程可以这样描述:
变量/函数-》栈(1)进程是一个实体。每一个进程都有它自己独立的地址空间。一般情况下要包括:代码区、数据区和堆栈。代码区的内容就是CPU执行的代码;数据区存储变量和进程执行期间使用的动态分配的内存,C程序员比较喜欢管这个叫堆;堆栈区存储过程调用的指令和局部变量,C程序员比较喜欢管这个叫栈。(2)进程是一个&执行中的程序&。程序是一个没有生命的实体,CPU赋予了程序有时限
的生命,这样它就成为了一个&活&的实体,我们称它为进程。(3)虽然程序的&生命&是CPU赋予的,但是这个机会却是人给的。人往往需要使用另
外一个进程(或者执行中的程序)才能让程序执行起来。那么让程序执行的进程,我
们称它为父进程。(4)进程会继承父进程的一些资源,这就如同孩子要继承父亲的基因一样。当一个进程
的父进程走完了它的&人生路&,那么这个进程就成了一个&孤儿&,我们称它为&孤
儿进程&。归结起来,进程与线程相比拥有十分显著的特点,那就是:资源独立、主从分明。
fork干了什么?虽然fork和fuck读音很近,但是想说明白fork干了什么可要比说明白fuck干了什么要复杂很多。那我们就从fork()系统调用发生时说起吧。当程序,更确切的说是进程执行了fork()系统调用,子进程会复制父进程的所有内存页面,并将其载入操作系统为它所分配的那片独立内存中。不难想象,这个拷贝的动作将会非常耗时(相对于CPU来说)。但是,看似很&笨&的Linux,这个时候&精明&了起来,因为它发现这么干不划算。为什么呢?因为谁都不知道fork()之后要干什么,如果是立即退出了呢,或者是执行&exec&系统调用呢(后面会说)?这等于是之前的努力要白费,人家没用啊。于是Linux引入了一种机制&&COW,根本就不干这种傻事。COW的全称是Copy On Write,翻译过来就是&写时拷贝&。也就是当fork发生时,子进程根本不会去拷贝父进程的内存页面,而是与父进程共享。但是这就有一个麻烦,因为进程的特点是&资源独立&,子进程跟父进程都共享内存了,那不就成了线程了吗?别说,Linux的线程就是返么干的。但是对于进程这么干不行,但是别着急,Linux有妙招。当子进程或父进程需要修改一个内存页面时,Linux就将这个内存页面复制一份给修改者,然后再去修改,这样从用户的角度看,父子进程根本就没有共享什么内存。这个花招就是COW,也就是进程要写共享的内存页面时,先复制再改写。对于线程来说,关掉COW就完事儿了。在这里你就应该能体会出Linux的线程和进程对于内核来说是一样的了吧。差别就在于用不用COW。采用了COW技术之后,fork时,子进程还需要拷贝父进程的页面表。采用这种设计就是要模拟传统Unix系统fork时的效果。当然,这种拷贝的代价非常的小,对于CPU来说都用不了几个时钟周期,类似nginx这类的高性能服务器系统就是受益于此。
mysql技术内幕InnoDB存储引擎P375
LVM使用了写时复制(copy-on-write)技术来创建快照。当创建快照时,仅复制原始卷中数据的元数据,并不会有数据的物理操作,因此快照创建速度
非常快。当快照创建完成,原始卷上有写操作时,快照会跟踪原始卷块的变化,将要改变的数据在改变之前复制到快照预留的空间里,因此这个原理实现叫
写时复制。而对于快照的读操作,如果读取的数据块是创建快照后没有修改过的,那么会将读操作直接重定向到原始卷,如果要读取的是已经修改过的块,则
将读取保存在快照中该块在原始卷上改变之前的数据。
lvm也有COW
但是还有一种情况需要说明,就是fork()之后进行execve()调用P530。这种用法跟Windows的CreateProcess()有点像,它们的实际效果也是一样的。当然,Linux下真正的多进程编程并不这样用,大多是只使用fork()。其实从本质上看,fork()与Windows的CreateThread()类似,甚至更公平地比较是与NtCreateThread()筹价。所以Linux下的多进程编程与Windows下的多线程编程拥有同等效率。而Linux的多线程设计,由于连COW机制都省略掉了,从这点上看就已经超越Windows了。当然,我在这里还得给Windows正名一下,不能将它一竿子打死。因为相对于Linux,Windows的设计更有弹性,它是一个多层次的而且更加组件化的一个操作系统。Windows有用许多子系统,我们通常说的Windows只是它的一个子系统,叫Win32或Win64子系统,所以也就有了WoW( Windows on Windows)的称呼。Windows的其他子系统还包括SFU、Posi和OS2。Windows NT内核也支持COW fork,但是只为SFU (Microsoft&s UNIX envlronment for Windows)所使用。看看这种设计,不服都不行啊,难怪Windows NT之父David Cutler被人称为&操作系统天神&啊(第一次知道,大开眼界)。我们老是拿Linux与运行Win32或Win64子系统的Windows进行对比,实在是太不公平了。
3. 进程的优势若说一项技术是否先进,从它诞生时间的新旧基本上就能判断了。但是有时候并不一定先进就是好的,有可能是激进的;也不能说原始就是笨拙的,有可能更加可靠。进程就是这样的一个典型例子。它相较于线程出现得更早,给很多人的印象也很笨拙,但是线程在很多时候有些过于激进,而进程则要可靠得多。主要体现在以下几个方面。第一,由于进程之间完全封闭,这是一种非常典型的面向对象所追求的封装特性。封装强调隐藏,让使用者能够不必关心对象内部的细节,这样也不会导致&善意的破坏&。封装也使得对象内部产生的破坏性行为不会波及无辜。进程将这种封装特性体现得淋漓尽致。其他进程不可能通过什么方便的方法去更改一个进程内部的数据或代码,一个进程由于某些bug导致崩溃也不会对其他进程有任何影响。因为它们所在的物理内存和能访问到的物理内存是完全孤立的。而线程强调数据的共享,这从本质上看就是对封装的一种破坏。很多时候为了避免多个线裎&撕扯&共享数据,不得不使用各种同步机制来&驯服&它们。而且一旦某个线程崩溃了,其他线程也不能幸免遇难,包括它所属的进程本身。所以在大多数情况下,多线程编程都是复杂的,而多进程编程则要简单许多。进程只需要做好自己的事情,崩溃了重新来过就行了。第二,Linux系统提供了丰富可靠的进程间通信机制。这使得进程与进程之间并不是完全独立老死不相往来的。套用到面向对象的概念就是对象向外的接口。进程也正是如此,可以利用进程间通信技术让两个进程彼此交换信息,也可以做到一个进程操作另外一个进程工作。而利用这种能力,进程之间完全可以像线程那样来交换数据或协调工作,但是完全不需要考虑复杂的同步机制,因为进程并不是通过共享数据来做到这些的。虽然直接共享数据更具效率,但是这点效率的提升并不能弥补由于需要同步而带来的复杂性的代价,而且同步做得太过火,根本就没有并行性可言了。更何况很多进程间通信机制在实现上也不见得比线程的直接内存共享方案差,这个我们在后面还会探讨。第三,也是最重要的,就是在Linux下进程的执行效率与线程的执行效率基本相当。至于为什么是这样,我们在前面就已经分析过了。就冲着这一点,基本上就没有理白在需要并发或并行运算的情况下不优先考虑进程。而作为更为先进的线程,基本上都是在某些特殊性况下的备选方案。当然,线程也有不可替代的情况,比如完全不需要数据同步的基于UDP协议的大数据量读取应用(流式视频播放器),在这种情况下线程则更为简单、方便且高效。也正是因为如此,Linux才不遗余力地弄出NPTL来。第四,也是进程所特有的,就是主从分明。所谓主从分明就是说进程有严格的父进程和子进程的概念,而且它们之间有很多的联系。比如父进程可以非常容易地了解到子进程出现问题退出了。利用这种机制就可以采用一种非常简单的方案来缩短系统故障的修复时间。当子进程因为有些因素不能正常继续运行的时候,干脆就直接退出,这时父进程就会感知到并重新启动这个子进程。而且进程退出的行为很多时候都可以不用交给程序来控制,操作系统能够干得很漂亮。充分利用这种机制可以获得很好的系统可靠性。而且程序员可以在很多时候不必去关心一些无关紧要的bug。因为系统会自动利用它那些与生俱来的机制帮你把故障修复时间做到最小,而很多系统只要满足这个要求就足够了。相反,对于线程则没有这样的机制。虽然线程也可以有父/主线程和子线程的概念,但是当有线程发生故障时,波及的恐怕是整个程序,即便有机制让父线程了解子线程已经不行了,也没有机会去修复了。由此可见,进程在大多数情况下都是具有优势的。这也是它即便古老而如今又依然能够充满活力的根本所在。在需要并发处理或并行运算时,考虑一下是不是多进程能够简化你的设计。与此同时,多个不同程序利用进程间通信机制互相协作来完成一个更富创意的功能,是目前任何机制都无法替代的。而且在未来很长一段时间内都会是这样。
15.1.3多进程程序开发不管理论说了多少,都不如来上那么一两个实例更有说服力。好,那么接下来我们就看看如何使用这个fork0来完咸多进程程序的开发。1. Fork()的用法
pid_t &0 父进程pid_t =0 子进程pid_t &0 失败
fork0=()系统调用非常简单,其完整定义如下:pid_tfork (void):这个调用简单到连参数都没有,只有一个返回值。但是这个返回值却有大文章。因为fork之后程序就会分叉了,这种分叉就导致不同分支的返回值有差别。如果这个返回值是0,那么则代表这是一个新的分支,也就是传说中的子进程了;如果这个返回值大于0,那么这个就在主干上,也就是传说中的父进程;那么这个返回值要是小于0呢?这个时候是没有分支产生的,也就是调用失败了。大多数的原因就是内存不够用了,或者进程太多系统不让创建了(受物理内存限制或管理员设定)。代码1对fork()系统调用的这种行为进行了很好的描述。代码1:
#include &unistd . h&
#include &stdio . h&
int main ( int argc, char *argv[] )
pid = fork ( ) ;
printf ( "error in fork! " ) ;
== pid ) {
printf ( "This is child process, pid is %d\n", getpid() );
printf ( "This is parent process, pid is %d\n" , getpid () ) ;
printf("var is %d\n", var);
代码1在我的电脑中执行结果是这样:
This is parent process, pid is
This is child process, pid is
通过var的值可以看出,最后一个printf()的输出是在两个不同的线程中的。其实代码1已经验证了前面所说的&复制&这个事实。这首先是因为fork()不需要额外的参数去传递一个类似线程的那种主函数;其次是fork()调用之后,执行的就是后续代码,没有任何迹象表明哪些是父进程特有的,哪些是子进程特有的。至于为什么代码能够执行不同的分支,主要是因为有if语句来判断了它的返回值。此外.对于最后的那个printf(),父进程和子进程都调用,只是var的值完全不同。为了进一步验证这个&复制&的事实,可以看下代码2,它的输出结果会有更好的证明。代码2:
#include &unistd . h&
#include &stdio . h&
int main ( int argc, char *argv[] )
, root = ;
printf ( "r\t i\t C/P\t ppid\t pid\n" ) ;
;i&;++i) {
printf( &%d\t %d\t parent\t %d\t %d\n",
root, i, getppid(),getpid() );
printf( &%d\t %d\t child\t %d\t %d\n",
root. i, getppid(),getpid() );
它在我的机器上执行结果为:
r i C/P ppid pid
其中r列代表是否为根进程;i列是i的值;C/P列描述是子进程还是父进程,ppid是进程的父进程 pid列就是当前进程的pid。将这个结果转化成图可能会容易识别,如图
由于进程的&复制&,导致i在子进程中会继承父进程的值,这样在pid为13049中的子进程中i值依然是0而可以继续去创建新的子进程,而pid为1的子进程则没有了这种机会。而且这个程序能工作,也恰恰是因为&复制&使得for循环在所有进程中都是完整的,可以持续运行。
2. 孤儿进程与僵尸进程在涉及多进程开发的时候,不得不谈论一下两个特殊类型的进程:孤儿进程和僵尸进程。所谓孤儿进程,在前面已经说过了,就是没有父进程的进程。但是这样的进程似乎不存在。因为在Linux下要执行一个程序一般都得通过shell、其他进程或imt进程。不管怎么样,都需要有一个进程的辅助才能启动一个新的进程。当然,有人说进程启动后作为父进程来创建一个子进程之后退出,这个子进程就是孤儿进程了。事实情况是这样吗?我们只需要稍微修改一下代码2就能验证这个说法。因为代码2中为了确保每个子进程都能有父进程,使用了sleep()系统调用让父进程等1秒再退出。我们只要将它去掉就行了。由于这个修改很简单,我就不提供代码了。况且勤动手是学习Linux的最佳途径,所以我也不想让你浪费掉这个动手的机会。修改后的代码,在我的机器上会得到下面这样的输出:
r i C/P ppid pid
我们已经预想到了,有些子进程的父迸程会退出。从这个输出结果上也有所体现,因为很多子进程的ppid已经发生了变化,但是它们的ppid依然是个1,说明依然有父进程。可见并没有子进程缺少父进程。如果一定要跟这个较劲,那么可以使用&ps axjf&命令来查看系统中所有进程的关系。然后你就会发现,大多数进程的的父进程不是1就是2。如果按照没有父进程的进程才是孤儿进程的定义,那么整个Linux系统中只有两个进程是,分别是init和kthreadd,它们的pid分别是1和2,ppid都为0,也就是没有父进程(不是很严格,0这个进程实际上代表Linux内核本身)。其实这两个进程是Linux系统中最特殊的两个进程init是所有用户进程的&祖先&,而kthreadd是所有内核线程的&祖先&。这个两个进程都有一个特别的爱好,就是喜欢&收养孤儿&。换句话说,所有以这两个进程其中之一为父进程的进程,很有可能就是孤儿进程。我说很有可能,是因为很多进程是它们的亲儿子(也就是它们直接创建的子进程,比如系统守护进程)。idle进程的pid是0从上述的分析看来,Linux系统中的孤儿进程还是很多的。但是一个世界中有这么多的孤儿却并没有让人感觉到任何凄凉,甚至还那么湍湍的温暖。困为在Linux这个世界中,有一个充满爱心的init和kthreadd,它们温暖着Linux的世界,人类世界要是也有init或kthreadd该多好啊!
第10章 生死与共的兄弟
/MYSQLZOUQI/p/5240409.html
④由于Linux的内核是一个整体(可以认为Linux内核就是一个大的进程),在它内部产生的多任务分支都叫内核线程(大进程里产生的任务分支只能叫线程了)。内核线程在被映射到用户空间时,很多都是我们通常所认识的进程。
10.5.5 kernel init如果你查看的是较老的内核源代码,或许找不到kemel init线程所对应的函数。没关系,这是因为较新的内核才这么叫。老的内核都叫init内核线程。不过在我看来新的称呼更加贴切。因为kemel&init内核线程实际上是Linux系统最为重要的init进程的内核部分。新的称呼更能体现它是在内核中这个特性。这个是Linux系统的第二个进程④,这也是init进程的pid -定是1的由来。而且在最新的内核代码中,由于必须在创建kemel init之前创建另外一个内核线程来做一些重要的事情,则不得不先创建kernel init,并把它锁起来。然后再创建新的内核线程,然后kemel_ init等到它执行完后解锁继续执行。如果不这么做,init进程的pid就会是2。那样的话,接下来所有的程序都可能会变的很2了。
ps axjf|grep kthreadd
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
: [kthreadd]
孤儿问题在Linux世界中解决得非常好,但是还有一个可怕的东西存在,那就是僵尸进程。它时刻都在威胁着Linux系统的祥和与安宁。僵尸进程很可怕,我提供一段代码来解释什么是僵尸进程。见代码3所示的内容。代码3:
#include &unistd . h&
#include &stdio . h&
#include &stdlib . h&
int main ( int argc, char *argv[] )
pid_t pid=fork();
printf( "This is child process. pid is %d\n&, getpid() );
从表面上看,这段代码并没有什么异常的地方。它只不过是循环的去创建进程,但是每个新创建的进程很快就退出了,也并不会产生什么内存或资源泄漏的问题。但是一个潜在的危机正在爆发,那就是僵尸们已经占领了你的后院,可是植物们还没有发现它们。要想发现这有什么问题,可以借助&ps ux&命令来观察。这段程序执行一段时间后,我的电脑中会有这样的进程信息:
Z:僵尸进程
+:前台进程 & defunct 死的
. /fork_zombie
pts/ z+ : : [fork_zambie] &defunct&
[fork_zombie] &defunct&
[fork_zombie] &defunct&
[fork_zombie] &defunct&
有很多进程的状态是我之前没有介绍过得,就是&Z&这个状态。你试图使用kill命令来杀掉它们是不可能的。而且你运用上所有能查到的杀死进程的办法都对处于这种状态的进程无能为力。当上述程序运行的时间越久,这种进程会越多,如果不去管他,最后整个系统都会充满这种进程,将内存消耗殆尽之后崩溃。这就是僵尸进程。这种进程与我们所了解到的对僵尸的描述非常相似:已经死了,但是躯体在,会伤害人,因为已经死了也就不存在杀死它的说法。如果你是《生化危机》迷,一定会对僵尸恨之入骨。当然,在《生化危机》中你还可以通过爆头、火烧等方法来对付那些僵尸,但是在Linux下你却完全无能为力。僵尸进程是进程已经死亡,但是没有人去&收尸&而导致的。按照Linux系统的设计,僵尸进程实际上几乎已经放弃了所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息。也正是因为这样,没有什么好的办法杀掉这个僵尸进程。由于最关心子进程退出状态的是创建它的父进程,所以大多数情况下负责给它收尸的就是其父进程。父进程通过使用wait()或waitpid()系统调用来完成这个收尸的工作。但是无论是wait()还是waitpid()都可能导致父进程的阻塞,这样也就失去了多进程并发的意义,所以需要通过一种机制使得父进程在正常工作的时候通知父进程去为它的某个子进程收尸。这种机制就是SIGCHLD信号。当然,如果父进程对子进程的退出状态不感兴趣,可以选择明确告知系统忽略SIGCHLD信号的方法,让系统给它的子进程收尸。此外,如果父进程退出,也就是说子进程成了孤儿,那么负责给它收尸的就变成了init进程。所以init进程的一项重要工作就是去收尸,可见它是多么的大爱无疆啊!不管怎么样,获取子进程的退出状态足父进程的大多数需求,所以wait()或waitpid()系统调用在多进程编程中都是非常重要的,需要掌握如何使用它们。它们的完整定义如下:
pid_t wait( int *status);
pid_t waitpid(pid_t pid, int*status. int options);
SIGCHLD-& 父进程-&waitpid -&收尸 -&调用1次收1条尸
其中wait()等价于waitpid(-1,&status,0)。从waitpid()的定义不难看出,第一个参数pid就是要被收尸的子进程的pid,status就是要获取的子进程退出状态。对于options参数,它决定了waitpid()的行为。可见waitpid()要比wait()灵活很多。虽然waitpid()的第一个参数是子进程的pid,但是我也说过wait()等价于它的一种特殊形式,就是pid等于-1的情况。这种情况表明要等待所有子进程的退出,具体是哪个子进程可以通过其获得返回值是不能确定的。当然,一次只能给一个子进程收尸,如果有多个子进程退出了,则需要多次调用。顺便说一下,这个pid参数还可以是0,那么它对应的同一个进程组的进程。甚至还可以是小于-1的值,那么它对应的是与这个值的绝对值柏同的gid所对应的进程组(比如-1024,那么对应的gid就是1024)。有关在程序中如何操作进程组内容,你可以通过Linux的联机帮助获得。需要注意的是通过它们获得的status不能直接使用,因为它的内容并不是子进程exit()调用的参数,也不是main()函数的返回值。要获得有用的信息需要通过若干个宏,比如WIFEXITED。本书不做详细阐述,你可以通过Linux的联机帮助获得这方面的信息。此外,options参数也比较有用。它有几个可用取值,且可以使用&|&位或操作联合使用,如果不想指定,给它传0就行了。比较有用的一个选项是WNOHANG,它可以使得waitpid()调用不会阻塞父进程,但是如果子进程没有退出,它也不会起到收尸的效果。一种可能的应用就是能够让父进程去轮训所有子进程的状态,这在很多时候比使用SIGCHLD信号要更好,程序的可读性也会比较好。当然,这个时候pid参数也最好是-1。至于其他的选项,可以通过Linux的联机帮助获得比较详细的介绍。代码4演示了fork()的一种常规使用方式,你可以拿来将它作为一个多进程开发的框架,大多数情况下都能套来使用。代码4:
#include &stdio .h&
#include &stdlib . h&
#include &errno . h&
#define PROC_COUNT 3
int create_process( pid_t *pid, int (*proc) ( void *arg ) , void *arg
fpid = fork ( ) ;
switch( fpid ) {
exit_code = proc( arg );
exit ( exit_code ) ;
int child_process( void *arg )
;i&;++i) {
printf( "process %d, i=%ld.\n", (long)arg. i );
int main ( int argc, char *argv)
pid_t pid [PROC_COUNT] ,
; i & PROC_COUNT; ++i )
create_process ( &pid [i ] . child_process , (void* ) i ) ;
fpid = waitpid( -, status, WNOHANG ) ;
== fpid ) {
; i & PROC_COUNT; ++i ) {
if ( fpid == pid[i] ) {
create_process ( &pid [ i] , child_process , (void* ) i ) ;
如果不做特殊处理,这个程序将会永远运行下去,甚至对子进程执行kill,也很快会被父进程重新启动。这个程序也不会有僵尸进程产生。
3. 启动外部程序多进程程序,其实并不仅限于程序本身是多进程的。如果能够启动外部程序而自身依然继续运行,并能够与这个新启动的程序进行合作,也是一种多进程程序开发的思路。这种类型的应用其实更为广泛,我在前面的章节就曾介绍过。若在程序中启动一个外部程序,需要使用execve()系统调用,它的完整定义是:
int execve (const char*filename, char *const argv[],
char *const envp[] );
该系统调用会执行名称为filename的二进制程序或脚本程序。需要注意,filename必须是程序的完整路径。后面两个参数argv和envp与C的main()函数后面的两个参数相互对应,而且main()函数的完整定义应该是这样:
int main(int argc, char *argv[],char *enxrp[] );
只是这个形式不经常使用。具体的你可以查找有关C语言的权威资料。execve()比较特别的地方是,它只有在失败的时候才有返回值。那么成功之后呢?很奇妙,新启动的程序与当前进程融为一体了。新启动的外部程序退出,当前进程也会退出。也就是说,在execve()后面有多少代码都不会被执行。代码5说明了这个问题。代码5:
#include &unistd. h&
#include &stdio . h&
int main(int argc. char *argv[],char *envp[] )
char *newargv[] ={ &Is&, &一l& ,NULL};
execve( &/bin/ls&, newargv, envp);
printf("execve calledl \n" );
这段代码的执行效果与直接执行&Is&l&命令没有任何两样,而且也不会输出&execvecalled&这个字样(如果你看到了,一定是程序写错了)。execve()会将其启动的程序与当前进程合并,绝大多数时候都不是我们所期望的结果。最起码执行完外部程序之后我们还希望我们的程序继续执行下去。要解决这个问题,可以使用fork(),在新建的子进程中执行execve()。然后我们的程序就可以继续做别的事情了。如果没什么可干的,也可以使用wait()等待子进程的结束。按照execve()的行为,新建的子进程就是这个新启动程序的进程,所以其退出状态也能通过wait()获得。顺便说一下,execve()是一个比较原始的系统调用,使用起来还是比较麻烦的,尤其是envp这个参数如果设置不好还会出现一些莫名奇妙的错误。其实Linux的API层还提供了另外5个接口,分别是:execl()、execlp()、execle()、execv()和execvp(),它们都是对execve()的封装,使用上更为简单。你可以通过Linux的联机帮助掌握它们的使用方法。这么简单的执行一下外部程序,顶多也就是获得一下它的退出状态,显然没有什么实际用途。但是当引入进程间通信之后,马上就变得有用得多了。虽然在命令行下如何做进程间通信你已经能够熟记于心了,但是如何在程序中进行进程间通信恐怕大多数人还不甚了解。那么我接下来就讲述这个话题。
15.1.4进程间通信的实现
新浪 TimYang /MYSQLZOUQI/p/4234005.html
进程接收信号有两种:同步和异步。同步信号比如SIGILL(非法访问), SIGSEGV(segmentation fault)等。发生此类信号之后,系统会立即转到内核陷阱处理程序trap命令,因此同步信号也称为陷阱。异步信号如kill, lwp_kill, sigsend等调用产生的都是,异步信号也称为中断。
其实Linux所提供的进程间通信机制有很多,受篇幅所限不能一一列举。我仅列举一些重要且十分常用的。它们是:信号、管道、I/O重定向、套接字。其他一些同样重要且常用的机制在其他章节也有所涉双,本节将不做复述。1. 信号信号是Linux系统中最为原始的一种进程间通信机制,是硬件的中断机制在软件层次上的一种模拟。那么在行为上,一个进程收到一个信号与CPU收到一个中断请求就是一模一样的了。而且也是通过一个数字来区分不同事件的,中断管这个叫中断请求号(IRQ),那么信号就是信号请求号了。信号是各种进程间通信机制中唯一的异步通信机制。一个进程不必通过任何操作来等待信号的到达,而且进程也不可能知道信号在什么时候会到达。掌握并能理解好信号机制,可以为学习Linux内核模块编程打下良好的基础。信号的发生主要有两个来源:硬件来源和软件来源。对于硬件来源,一般是按下键盘特殊按键组合或某些硬件故障;对于软件来源,最常见的就是那些发送信号的系统调用:kill()、raise()、alarm()和setitimer(),以及比较新的sigqueue()。信号有可靠与不可靠的分别。在早期Unix系统中的信号机制比较简单和原始,如果进程不及时处理就会出现丢失的情况。此外,进程每次处理完一个信号后,这个信号的处理函数将被系统复位,即之前设置的信号处理函数会失效。为了能够反复处理这个信号,就不得不在信号处理函数的末尼重新向系统申明使用该函数来处理信号。Linux对信号的这种处理机制做了改进,使得信号处理函数始终能用。随着时间的发展,实践证明信号会丢不是什么好事儿。但是Unix的历史太过悠久,要改变这种现状已经不太可能了,毕竟无数多的应用已经这么用了。但是增加一下新的信号,让它们是可靠的就很好办了,也就是那些信号请求号介于SIGRTMIN和SIGRTMAX之间的信号。需要注意,只有在这之间的信号是可靠的,而且无论采用什么方法都是可靠的。除此之外的都是不可靠的,无论采用什么方法都是不可靠的。可靠信号是通过引入信号队列来实现的,即没有处理过的信号会在队列中,因此不会丢失。人们还给可靠信号和不可靠信号各起了一个文绉绉的名字:实时信号和非实时信号。毕竟把信号说成可靠或不可靠,在外行眼里还以为是什么不靠谱的东西呢。这名字改得多&实在&啊!前面说过,信号的软件来源是一些系统调用,那么我们就来认识一下它们。它们的完整定义如下:
int kill (pid_t pid, int sig) ;
int raise (int sig) ;
unsigned int alarm (unsigned int seconds) ;
int setitimer (int which, const struct itimerval *new_value)
struct itimerval *old_value) ;
int sigqueue (pit_t pid, int sig, const union sigval value) ;
Kill()可以向任意进程发送任意信号,pid参数就是目标进程的pid,而sig参数就是具体的信号请求号。之所以使用kill这么可怕的名称,是因为进程在接到大多数信号的默认处理方式都是自毁。kill()的pid参数与waitpid0的pid参数类似,只是前者的范围扩大到整个系统范围,而后者仅是调用者本身的子进程。需要注意的是,当pid等于-1的时候,不会对init进程有任何影响。此外,kill()所能影响的进程也是它有权限访问的进程(root权限可以向任何进程发送信号,非root权限只向相同拥有者的进程发送信号)。raisel()与killl()十分类似,其实完全等价于kill(getpidl(),sig)。所以不难看出它是向进程本身发送信号。alarml()只会向进程本身发送SIGALRM信号。这个信号又称为闹钟信号,所以alarm()唯一的参数就是要说明多久以后发送这个信号,时间按秒计。比较有意思的是它的返回值,也是一个按秒计的值。这个值是上一次还没有发出的SIGALRM信号与本次调用的剩余时间间隔。需要注意,如果上次的信号没有发出去,本次的调用会使它永远都发不出去了。setitimerl()育点类似于alarml()的封装,即当进程收到一个SIGALRM信号之后继续调用alarml()来完成一个定时器的操作。setitimerl()实际上按照不同的需求提供了3种类型的计时器:ITIMER_ REAL、ITIMER_ VIRTUAL、ITIMER_ PROF。ITIMER_REAL类型的定时器采用绝对时间来计时,即只要经过指定的时间就会向进程发出SIGALRM信号;ITIMER- VIRTUAL类型的定时器采用程序运行时间来计时,必须等程序实际运行了指定的时间才会向进程发出SIGALRM信号;ITIMER_PROF介乎于前两种计数器之间,它按照系统处理这个进程的整体时间计时(包含程序本身和操作系统调度它所消耗的时间)。给setitimerl()设置时间间隔是其第二个参数,第三个参数没什么实际的用途。这是一个有点怪异的结构体,定义如下:
struct timeval {
struct itimerval {
struct timeval it_
struct timeval it_
怪异就怪异在需要设定一个第一次发出SIGALRM信号酌时间,再设定一个间隔时间。按照大多数人的经验,只需要设置一个间隔时间就满足需要了。所以大多数情况都会将这两个值设置为相同的值。Sigqueue()是新引入的一个系统调用,也是功能最强大的。前面所介绍的这些仅能发出信号,但是sigqueue()不但能发出信号请求,还会给这个信号附带一份数据。其第三个参数就是干这个的。它是一个联合体,定义如下:
union sigval {
int sival_
void *sival_
所以可以附带一个整数或一个指针。如果需要传递复杂的数据结构了,显然需要使用指针了。发出信号的系统调用基本上算是介绍完了,接下来要看一下如何去处理收到的信号。有两个系统调用来干这事儿,它们是:signal()和sigaction()。对任何收到的信号,其实进程都会有默认的处理方式,只是这个默认的处理方式有些悲壮,大多都会导致进程退出,看来进程有一些日本的武士道精神。使用signal()和sigaction() & kill命令为什麽叫kill能够改变进程的这种武士道精神。它们的完整定义如下:
sighandler_t signal (int signum, sighandler_t handler) ;
int sigaction( int signum, const struct sigaction *act.
struct sigaction *oldact) ;
2. 管道在Linux系统中,管道是一种使用非常频繁的进程间通信机制。从本质上说,管道也是一种文件,只是它和普通文件有很大的不同。管道不能像普通文件可似无限大(相对而言),因为管道实际上只是内存中的一个固定大小的缓冲区。在Linux系统中,这个缓存区大小为1个内存页面,即4K字节(32位系统,在64位系统下是1M字节)。当这个缓冲区满的时候,再向管道中写入数据就会被阻塞,直到缓冲区中的内容被读取出来有了空闲的地方,写操作才继续进行。与此相反,如果这个缓冲区空的时候,从管道中读取数据也会被阻塞,直到缓冲区有内容之后,读操作才能继续进行。需要注意的是,管道是单向的,即只能向一端写,从另一端读;管道中的数据也是一次性的,即一旦有人读取,其他人就读不到了。也正式因为这些特点,数据就象水在水管中流淌一样,从水管流向水龙头,水龙头关上了,水管的水也就不流了。管道也因此而得名。在命令行下如何使用管道在本书开头的章节中就已经介绍了。在这里我将向大家介绍如何在程序中使用管道。在程序中是通过pipe0系统调用来创建管道的,它的完整定义如下:
Pipe()调用成功之后会返回两个文件描述符,一个用于读,另一个用于写。这两个文件描述符通过pipe()这个奇怪的参数来获得,这个参数正好是一个拥有两个元素的整形数组。pipefd[0]倮存用于读数据的文件描述符,pipefd[1]则保存用于写数据的文件描述符。既然管道的本质是文件,就可以利用write()和read()这两个系统调用来读写数据了。代码6演示了两个进程通过管道来传递数据。代码6:
#include &s tdio . h&
#include &stdlib . h&
#include &unistd . h&
void child ( int fd )
write ( fd. &i. sizeof ( i ) ) ;
int main ( int argc, char *argv [ ] )
pipe( pfd);
pid = fork() ;
== pid ) {
close( pfd[] ) ;
child( pfd[] ) ;
close ( pfd[] ) ;
For (i=;i&;++i) {
read( pfd[] , &datar sizeof ( data ) );
printf ( "data: %ld\n". data ) ;
waitpid( pid, NULL, O );
这段代码能够正确执行,依然是基于进程&复制&原理的。子进程完全复制了父进程的管道文件。这个时候,无论是在父进程还是在子进程中向管道中写入数据,它们都能从管道另一端读取数据。显然,通过这种简单的机制,就可以通过创建一个管道的方法,使得子进程和父进程互相交换数据。但是根据管道的特性,谁先去读取数据,数据就归谁了,会使得父子进程需要同步读写才不至于弄丢数据。而且我们的实际需要是子进程向管道中写入数据,父进程从管道中读取数据。那么我们就在父进程中关闭管道的写入端,在子进程中关闭管道的读取端。一来是节省资源,二来避免不必要的麻烦。即便真的需要利用管道来进行父子进程的通信,也建议使用两个管道来避免逻辑上的复杂性。
3.I/O重定向在命令行中使用管道其实还隐含了I/O重定向的应用,即将管道前端进程的标准输出重定向到管道后端进程的标准输入中。这样的机制也可以通过程序来完成。而且这种I/O重定向机制也不仅仅是只能使用管道的,利用其他方法也能完成,比如套接字。进行I/O重定向需要使用dup20这个系统调用,它的完整定义如下:int dup2(int oldfd. int newfd);这介系统调用会将newfd关闭,然后将它做成oldfd的拷贝。dup2()调用成功的前提是oldfd必须有效。因为如果oldfd无效,这种复制行为也就是无效,newfd也就白白地被关闭了。此外,如果newfd和oldfd相同,dup2()虽然不会调用失败,但是也是无意义的。Dup2()之所以能够实现I/O重定向,就是因为这种复制机制。比如我们有两个打开的文件A和B,对应的文件描述符分别是oldfd和newfd。当调用dup2()后,newfd被关闭且变成了oldfd的拷贝,此时再通过newfd向文件B中写入数据,实际上写的是文件A。这就相当于是将文件B的写操作重定向到文件A了。依此类推,读操作也重定向到A了。既然标准输入和标准输出也是两个文件,那么利用此方法就可以将它们重定向到任意文件,包括管道甚至网络。代码7对代码5进行了修改,结合代码6并加入I/O重定向,使得我们能够获得&Is一l&命令的输出。代码7:
#include &stdio . h&
#include &stdlib . h&
#include &string . h&
#include &unistd . h&
#define BUF_SIZE 1024
void child ( int fd, char *envp [ ] )
char *newargv [ ] = { "ls " , & -l" , NULL } ;
dup2 ( fd, STDOUT_FILENO) ;
execve ( " /bin/ls " , newargv, envp ) ;
int main ( int argc, char *argv [] , char *envp [] )
pipe ( pfd ) ;
pid = fork() ;
== pid ) {
close (pfd[] ) ;
child( pfd[l] . envp ) ;
close (pfd[] ) ;
memset (buf, , BUF_SIZE + ) ;
== (read (pfd[] , buf, BUF_SIZE))) {
printf ( "%s", buf ) ;
waitpid( pid,NULL, );
虽然这个例子从执行的效果上看,与代码5没什么两样。但是只要将printf()那条语句注视掉,就得不到任何输出了。这是最好的佐证。顺便说一下,使用C语言编程为了实现代码7的效果其实完全没有必要这样庥烦,使用popen()这个函数就能够搞定一切:)。
15.2 离别钩:VFS《离别钩》是古龙晚期作品,算是《七种武器》中写得最好的。故事是在二元对立的参差之美中展开的:狄青麟是世袭一等侯、天下第一风流侠少;杨挣是江湖大盗的后人、县衙的小捕头。杨挣有力量对抗狄青麟的阴谋吗?狄青麟一身白衣如雪,用温柔多情方法杀人,他拥有一座巨宅,却没有&家&。他是大恶中的大恶、大奸中的大奸,但与《笑傲江湖》中的&君子剑&岳不群有天壤之别。岳不群坏得让人厌恶,狄青麟坏得让人欣赏,因为那是一种近乎本色的坏&&他别无选择,那就是他的命运,他的生活。他杀朋友,杀情人,杀师父,因为他只爱他自己,他心中本来就没有朋友、情人和师父。杨铮昵,命贱如泥土,他有爱,有决心,面对外来的压力,他没有屈服,也没有崩溃。他拿起了离别钩&&&你为什么要用如此残酷的武器?&&因为我不愿被人强迫与我所爱的人离别。&&&&你用离别钩,只不过为了要相聚。&最后,杨挣的离别钩战胜了狄青麟的薄刀。就因为要相聚,杨挣胜利了。Linux也是因为要相聚,所以它也胜利了,而且还是绝对的胜利。因为没有任何一款操作系统能够像Linux那样支持如此之多的完全不同的文件系统。Linux的武器就是VFS,而这一切的动力都是因为一句口号&&
15.2.1 一切都是文件本书在前面就介绍过,在Plan9咐代Unix就有了一句&震人心脾&的口号:一切都是文件,也就是说不管是普通的文件、硬件外设甚至是网络,在Unix中都会被当成一个文件来看待。从磁盘上读取文件数据,从鼠标、键盘等硬件外设获得输入,接收来自网络的数据等,都可以看作是从&文件&中读取数据;把数据保存在磁盘上的文件里,将文字显示在屏幕上,播放音乐和电影,通过网络给大洋彼岸的友人传递信息等,都可以看作是向&文件中&写数据。这个高度统一的接口让人与计算机打起交道来十分地爽快和便利,因为人不需要知道实际操作的是什么,只要使用系统提供的read或write接口就能搞定。这么牛B的特性,Linux哪有不去&山寨&它的道理?但是,不管是保存在磁盘上的真文件也好,还是代表硬件外设的假文件也罢,更不用说网络这种没边儿的文件多么让人&春心荡漾&,要将它们这么高度统一地抽象在一起,可不是一件容易的事儿。更何况Linux还是一个很有&量&的操作系统呢?你可能要问,说Linux有&量&该从何说起呢?答案应该是显而易见的,Linux本身就能支持近百种不同的文件系统这是事实,Linux支持的特种文件系统有多么千奇百怪也是本书前面讨论过的重点,而且Linux逐有尽早进入用户空间的远大抱负,你能说这不是有&量&吗?那么Linux是怎么做到这么有&量&的呢?因为Linux有一颗能够不断生长的&树&&&
15.2.2一棵有生命的&树&如果你没有跨章节看书的习惯,那么到了这里应该已经读完了第3章一棵&树&的奥秘,那么承载着Linux操作系统及其所有软件和数据的这棵&树&长什么样应该有所了解了。如果你真的喜欢跨章节看书的话,那么就请你现在转到第3章,这里我们还要继续看一看这棵&树&,因为这是一颗能够不断生长的树。最近这几年磁盘技术发展得快,100G、200G、500G、IT、2T这样地翻着翻儿地涨容量,但是说Linux有一棵能够不断生长的&树&跟这事儿没关系。因为换了磁盘就等于这颗&树&就死了,顶多是换成了一棵更大的&树&,跟生长没有关系。但是为了保持Linux这棵&树&活着,没人拦着不让你再添加新的磁盘。要使用这个新添加的硬盘怎么办呢?显然很好办啊,mount嘛,你早就知道了。是不是mount之后,Linux这棵&树&上就长出了新的&枝叶&了?答案显然是肯定的。看,生长了吧!可以让Linux这棵&树&生长的方法不止是添加新的磁盘。在&一切都是文件&这个口号的驱使下,您添加一个声卡Linux的这棵&树&会长一点,您插入一个U盘Linux这棵&树&会长一点,甚至你执行一个程序Linux的这棵&树&都会长一点&&这个现象在第9章也详细地探讨过了,没看的现在就看看吧。可以说Linux的这棵&树&是随时随地都在生长的,当然也有&死掉&的枝叶,那么更为确切的描述就应该足有新陈代谢了。有新陈代谢不就是生命了吗?这时,Linux的这棵树是&活&的,有生命的。这一切都是因为VFS。那么接下来我们就看看VFS如何工作的吧。
15.2.3 VFS简介VFS是一种软件机制,它的全称并不是大多数人想象的Virtual File System,而是Virtual Filesystem Switch,翻译过来就是虚拟文件交换系统。VFS负责Linux的文件系统的管理,所以更为正式的叫法应该是Linux的文件管理子系统。Linux的这种文件管理子系统比较特别,它是虚拟的,也就意味着它并不直接跟真正的磁盘文件有任何关系,或者说与VFS有关的数据结构只存在于物理内存中。这并不是我在忽悠,这是事实。这些数据结构在使用的时候就建立,不用的时候就删除。也就是因为这样,才能让Linux的这颗&树&拥有生命,让人们看起来它是&活&的。当然,如果只有VFS,Linux系统是无法工作的。因为它的这些数据结构不能凭空捏造出来,必须与具体的文件系统相结合,如ext4、btrfs、procfs等,才能够开始实际的工作。为了与VFS这种&虚拟&的文件系统相对应,我们将ext4、btrfs、procfs等称作实体文件系统。在Linux内核的运行过程中,在执行具体文件操作的时候,所有其他子系统只会与VFS打交道,都是由VFS转交给具体的实体文件系统完成的。从这个角度上看,VFS是所有实体文件系统的管理者,也是内核其他子系统的通用文件处理接口。图15.2清晰地描述了VFS在Linux内核中的逻辑关系。
VFS既然要作为一个通用的文件处理接口,那么它就必须制定一个统一的规范来要求所有实体文件系统去遵循。做这种规范的一个最简单的做法就是面向对象的多态机制&&VFS提供接口定义,实体文件系统去实现具体的接口。VFS就是采用这种方式,只是多态机制被C++这类面向对象的语言做了内在支持,而Linux内核使用C语言开发就需要使用一些技巧了。具体的技巧我们这里就不说了,大家可以从很多地方了解到。接下来我们结合一个实际例子来展示VFS在Linux内核中所处的位置。而这个例子并不是那些常规的文件系统,而是网络应用中的非常重要的socket。在开始讲述之前,我们先来了解VFS的一些基本数据结构。
15.2.4基本数据结构VFS中有4种主要的数据结构,它们是:1. 超级块( superblock)对象用于保存系统中已安装的文件系统信息。对于基于磁盘的实体文件系统,超级块对象一般对应于存放在磁盘上的文件系统控制块。也就是说每个实体文件系统都应该有一个超级块对象。2. 索引节点(inode)对象用于保存具体文件韵一般信息。对于基于磁盘的文件系统,索引节点对象一般对应于保存在磁盘中的文件控制块(FCB)。也就是说每个文件都应该有一个索引节点对象。每个索引节点对象都有一个索引节点号,用于唯一标识某个实体文件系统中的一个具体文件。3. 目录项(dentry)对象用于保存文件名、上级目录等信息,正是它形成了我们所看到的Linux这棵&树&。目录项对象完全是在内存中的,会根据实际需要动态建立。
清空缓存:
&/proc/sys/vm/drop_caches
4. 文件(file)对象 & 文件描述符/句柄用于保存已打开的文件与进程之间进行交互的信息。这类信息也是完全保存在内存中的,且仅当进程访问文件期间才有效。也就是说,当进程打开文件就会创建一个文件对象,当进程关闭文件,对应的文件对象就会被释放。其中每种对象都包含一个操作对象,依次为super_operations、inode_operations、dentry_operations,以及file_operations。我们的文件系统只需要实现对应四个对象的操作方法,然后把它们注册到内核就可以了。
15.2.5 sockfs这部分理解上有点难度,还需要分析几个数据结构。第一个就是file_system_type结构:
struct file_system_type{
const char *
int fs_flags;
//get_sb最关键的函数,用来得到文件系统的超级块。
int (*get_sb) (struct file_system_type*,int, const char*,void*,struct
Vfsmount*);
void (*kill_sb) (struct super_block*);
这个结构表示了一个文件系统。然后就是vfsmount结构:
struct vfsmount {
struct list_head mnt_
struct vf smount*mnt_parent; /* fs we are mounted on */
struct dentry *mnt_mountpoint; /*dentry of mountpoint*/
Struct dentry *mnt_root; /*root of the mounted tree*/
Struct super_block *mnt_ /*pointer to superblock*/
Struct list_head mnt_ /*list of children. anchored 锚点 抛锚here*/
Struct list_head mnt_ /*and going through their mnt_child*/
bytes hole on 64bits arches*/
Const char *mnt_ /*Name of device e.g. /dev/dsk/hdal*/
Struct list_
Struct list_head mnt_ ,*link in fs-specific expiry list*/
Struct list_head mnt_ /*circular listof shared mounts*/
Struct list_head mnt_slave_list;/* list of slave mountS*/
struct list_head mnt_ /* slave list entry */
struct vfsmount *mnt_ /* slave is on master-&mnt_slave_list */
struct mnt_namespace *mnt_ /* containing namespace */
atomic_t ___ mnt_
它描述的是一个独立文件系统的挂载信息,表示了一个安装点,换句话说也就是一个文件系统的实例。每个不同挂载点对应一个独立的vfsmount结构,sockfs文件系统的所有目录和文件隶属于同一个vfsmount,该vfsmount结构对应于该文件系统顶层目录,即挂载目录。如果文件系统是sockfs文件系统,type-&get_sb实际上就是sockfs_get_sb,它就是把sockfs_ops所属的super_block结构挂接到全局链表super_blocks中,通过这么一个操作,形成了&super_block,sock_fs_type,sockfs_ops&三元组,这样协议栈与用户层的连接关系就基本确定了,Linux酌socket文件系统就建立起来了。第三个是files struct结构:
struct files_struct {
//大部分只读
struct fdtable *
//SMP独立cache上可写部分
spinlock_tfile_lock____cacheline_aligned_in_
struct embedded_fd_set cloSe_on_exec_
struct embedded_fd_set open_fds_
//文件结构体代表一个打开的文件,
//系统中的每个打开的文件在内核空间都有一个关联的struct file
struct file*fd_array [NR_OPEN_DEFAULT];
它主要是为每个进程来维护它所打开的句柄。需要注意的是fd_array和fstable中的fd的区别。当进程数比较少也就是小于NR_OPEN_ DEFAULT(32)时,句柄就会存放在fd_array中,而当句柄数超过32时就会重新分配数组,然后将fd指针指向它。我们通过fd就可以取得相虚的file结构。提醒一下,files_struct是每个进程只有一个的。file_structs里面使用到fdtable结构如下:
struct fdtable {
unsigned int max_
struct file** /*current fd array*/
fd_set *close_on_
fd_set *open_fds;
struct rcu_
struct fdtable *
通过以上四个结构我们大致了解了Linux文件系统的组成。Linux的文件无处不在,设备有设备文件,网络有网络文件&&Linux以文件的形式实现套接字,与套接字相应的文件属于sockfs特殊文件系统,创建一个套接字就是在sockfs中创建一个特殊文件,并建立起为实现套接字功能的相关数据结构。换句话说,对每一个新创建的BSD套接字,linux内核都将在sockfs特殊文件系统中创建一个新的inode。
[root@steven ~]# df -iTH
Filesystem Type Inodes IUsed IFree IUse% Mounted on
/dev/mapper/VolGroup-lv_root
tmpfs tmpfs 128k
128k % /dev/shm
/dev/sda1 ext4 129k
128k % /boot
从df -iTH看,所有文件系统都有inode,包括tmpfs和sockfs,所有文件系统都有superblock,因为执行df命令都能很快出结果无论当前系统挂载了多少种文件系统
/MYSQLZOUQI/p/5252245.html
[root@localhost /]#ls &id
[root@steven ~]# ls -id /boot
由此可知,根目录的inode值为2。
利用ext3grep恢复文件时并不依赖特定文件格式。首先ext3grep通过文件系统的root&inode(根目录的inode一般为2)来获得当前文件系统下所有文件的信息
15.3孔雀翎:mmap(内存映射)古龙笔下的《孔雀翎》是一种早已不存在的暗器,高立向朋友秋风梧借来孔雀翎,信心十足地杀了强敌之后才发现孔雀翎已经丢失。而秋风梧告诉他,其实孔雀翎早就没有了,他借给高立的只是&信心&。&真正的胜利,并不是你用武器争取的,那一定要用你的信心。无论多可怕的武器,也比不上人的信心。&我将Linux的mmap对应为孑L雀翎,因为它似有似无,小巧而&致命&。很多时候你并不知道它到底能干什么,但是只要你有足够白信的理念,一切事情忠于自己的内心、洒脱面对一切,或许会有你意想不到的事情发生。真的是这样吗?那我们就看看mmap是否真跟你想象的一样&&
15.3.1 理解mmap提起mmap,大多数了解它的程序员马上就会想起一个很牛X的名词&&内存映射文件。为什么会这样呢?因为在这个还是由Windows主宰的桌面电脑世界里,Windows已经向程序员们传达了足够丰富的知识,这其中就包括内存映射文件,而且还说这种方法能够加速文件的读写。当这些程序员转向Linux开发的时候自然就会去对号入座,发现Linux也有内存映射文件这个东西,而且就是通过mmap0这个系统调用来实现的。那么按照在Windows下养成的惯性思维,自然就会认为mmap0只是用来干这个的,因为Windows就提供了一个专用API-CreateFileMapping()来做这事儿。而且mmap()系统的一个参数也是文件描述符,这就更加肯定了这种想法。如果大家都持有这种想法那将是很悲剧的事情,因为这等于没有真正掌握Linux到底怎么用,也就无法真正发挥Linux的威力了。实际上,mmap是一种机制,是Linux进行虚拟内存管理的核心机制。表面上看mmap能够让用户将某个文件映射到自己程序地址空间的某个部分,使用简单的内存访问指令就能对这个文件进行读写。实际上,这就是Linux内核本身的组织模式。换句话说,Linux内核将其整个内存地址空间看作是一系列不同&文件&的映射,只是在这里它们有另外的名字&&内核对象。再进一步说,Linux加载内核的过程,就是做内存映射的过程。甚至加载可执行文件也是利用内存映射完成的。根据这个事实,有人应该可以推论出:Linux的内核文件以及可执行文件的结构,应该与它们在内存中的结构是一样的。Linux为什么要这么做呢?先来看看读写文件的传统方法有什么毛病吧!如果按照传统的方式来读写文件,首先必须使用open()系统调用打开这个文件,然后使用read()、write()以及lseek()等系统调用进行顺序或随机的读写。这种方式的效率是极其低下的,因为每一次读写都要进行一次系统调用,毕竟浪费掉这么多无用的CPU时钟还是说不过去的。另外,如果有许多进程访问同一个文件,那么每个进程都需要在自己的地址空间维护一个副本,这又是一种极大的资源浪费,即使内存已经是白菜价了浪费掉也很可惜。当然,举这个例子有点牵强,毕竟不同的进程读写一个文件,如果不想有&灵异事件&发生,还真得搞几个副本出宋。那么作为一个进程肯定是要有一个程序文件与它对应的这种事儿前面已经说过了。Linux作为一个支持多进程的操作系统,一个程序文件搞出几个甚至几十个进程出来也不是什么难事儿。作为进程,除了数据会变化外,代码肯定是不会变的,否则就中毒了。那么每个进程在内存中都有这么一个程序文件的副本,就真的太浪费了。用mmap读写文件就没有传统方法的那些毛病!因为它把文件当作内存来看待。利用mmap,对一个文件做普通的读写,跟读写内存没什么两样,都是一些指针的操作,根本不需要管什么系统调用,自然就不浪费CPU的时钟了。另外,由于mmap玩的是虚拟内存,虽然不同的进程看到的内存区域可能不同,但那毕竟是虚的,实际的物理内存区域可以是一个,这可就是实在的了。不但多个进程访问同一个文件可以不用维护多个副本,多个进程本身也不需要多个副本,自然就不浪费&宝贵&的内存了。此外,由于Linux 一直都秉承着一切皆文件的方针来做事情,那么将设备文件做内存映射,就使得对设备的控制像访问内存那样简单,也避免了I/O操作,从而提高了系统的整体效率。有些人可能会疑惑,mmap怎么能把文件当作内存使呢?要回答这个问题,就要搞清楚什么是虚拟内存。接下来我们就看看虚拟内存是个什么玩意儿。
15.3.2虚拟内存技术虚拟内存应该是大多数程序员们所耳熟能详的计算机科学中的一个基本概念。但是真要较起真儿来,还真没有几个人能说清楚这个虚拟内存到底是怎么回事儿。很多人还很奇怪,为什么Linux进程的起始地址都是一样的,它们怎么就不互相&打架&呢?电脑的内存只有2G,可是每个进程都有4G的虚拟内存,而且有好几十个进程一起跑,2G怎么够用呢?原因就是虚拟内存是&假&的,跟实际的物理内存不是一回事儿。大部分人是在学习操作系统原理的时候了解到虚拟内存这个概念的,但是虚拟内存与操作系统本身并没有多大关系。虚拟内存是CPU的硬件特性,而操作系统要负责的是对它的管理。如果某个CPU不具备虚拟内存的特性,操作系统即便支持对虚拟内存的管理也是白搭,它管谁呢?虚拟内存是能够开发出多任务操作系统的关键特性,所以对于一个不支持虚拟内存的CPU来说,是很难在它上面运行多任务操作系统的。但是有人会说:现在都是21世纪了,还有不支持虚拟内存的CPU?是

我要回帖

更多关于 docker 指定内存大小 的文章

 

随机推荐