本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-process/ Linux 中进程通过 fork() 被创建时,它差不多是和父进程一样的,它得到父进程的地址空间拷贝,运行和父进程一样的代码,从 fork() 的后面开始执行,父进程和子进程共享代码页,但子进程的 data 页是独立的(包括 stack 和 heap)。 早期的 Linux kernel 并不支持多线程的程序,从 kernel 来看,一个多线程的程序只是一个普通的进程,它的多个执行流应该完全在 user mode 来完成创建、处理、调度等操作,例如使用 POSIX pthread 库。当然这样的实现是无法让人满意的,Linux 为此使用轻量级进程为多线程程序提供更好的支持,两个轻量级进程可以共享资源(例如:地址空间、打开的文件等等),一个比较简单的方法是将为每个线程关联一个轻量级进程,这样每个线程可以被 kernel 单独调度,使用 Linux 轻量级进程的库有:LinuxThreads、NPTL、NGPT 等。Linux kernel 同时也支持线程组(可以理解为轻量级进程组)的概念。 1、进程描述符: 进程描述符由 task_struct 结构来表示,一般来说,每个可以被独立调度的执行上下文都必须有自己的进程描述符,因此尽管轻量级进程共享了很大一部分 kernel 数据结构,它也必须有自己的 task_struct。task_struct 中包含关于一个进程的差不多所有信息,它定义在 include/linux/sched.h 文件中,你会看到这是非常大的结构,其中还包含指向其它结构的指针。访问进程自身的 task_struct 结构,使用宏操作 current。 task_struct 中的 struct mm_struct *mm 即指向进程的地址空间。task_struct 的 state 字段表示进程的运行状态,取值有 TASK_RUNNING(正在运行或正在队列中等待运行,进程如果在用户空间只能为此状态)、TASK_INTERRUPTIBLE(可响应信号)、TASK_UNINTERRUPTIBLE(不响应信号)、TASK_STOPED 等,另外 state 还有特殊的两个值是 EXIT_ZOMBIE(僵尸进程) 和 EXIT_DEAD(进程将被系统移除)。kernel 提供 set_task_state 宏修改进程状态,set_task_state 最终调用 set_mb,set_current_state 用于当前进程的状态。task_struct 的 pid 字段就是咱们喜闻乐见的进程 ID 了。 这是一个典型的 Linux 进程状态机图: POSIX 1003.1c 标准规定一个多线程程序的每个线程都应该有相同的 PID,这样的好处是例如发一个信号给一个 PID,一个线程组里的所有线程都能收到。同一线程组中的线程有相同的线程组号(Thread Group ID),线程组组号放在 task_struct 的 tgid 成员变量中,一般是线程组里的第一个轻量级进程的 PID。特别需要注意 getpid() 系统调用返回的就是 tgid 的值,而不是 pid 值,这样一个多线程程序的所有线程可以共享一个 PID。 对每个进程,kernel 在通过 slab 分配器分配 task_struct 时,通常是实际分配了两个连续的物理页面(8KB),以 thread_union 联合表示,其中包括一个 thread_info 结构(其 task 成员是指向 task_struct 的指针)以及 kernel 模式的进程堆栈。esp CPU 堆栈指针即表示此进程堆栈的栈顶地址,进程从用户模式切换到 kernel 模式时,kernel 堆栈会被清空。为了效率考虑,kernel 会将这两个连续的物理页面的第一个页面按 2^13(也就是 8KB) 对齐,为了避免内存较少时产生问题,kernel 提供配置选项(就是下面的 THREAD_SIZE 了)可以将 thread_info 和堆栈包含在一个页面也就是 4KB 的内存区域里。一般来说,8KB 的堆栈对于内核程序已经够用。 看看 Linux 2.6.34 中 thread_union 的定义: 由于 thread_info 和内核堆栈是合并在连续的页面里的,kernel 就可以从 esp 指针得到 thread_info 结构地址,这是通过 current_thread_info 函数来实现的。 假设 thread_union 是 8KB 大小,也即 2^13,将 esp 的最低 13 位屏蔽掉即可得到 thread_info 的地址,如果是 4KB 的栈大小,屏蔽掉最低 12 位即可(和上面的代码一致),这样通过 current_thread_info()->task 就能得到当前的 task_struct,这就是 current 宏的实现了。 系统中进程的列表保存在 init_task 所在的双向链表中,task_struct 的 tasks 字段就是 list_head,init_task 表示的就是 PID 为 0 的 swapper 进程(或者叫 idle 进程),其 tasks 会依次指向下一个 task_struct,PID 为 1 的进程就是 init 进程,这两个进程都由 kernel 来创建。 而关于可以运行的进程的调度,Linux 2.6.34 和 ULK 上说的已经有很大的不同了。2.6.34 上加上了 struct sched_class 结构体表示不同类型的调度算法类,目前 2.6.34 上实现了三种:Completely Fair Scheduling (CFS) Class(完全公平算法,见 kernel/sched_fair.c)、Real-Time Scheduling Class(实时算法,见 kernel/sched_rt.c)和 idle-task scheduling class(见 kernel/sched_idletask.c),这三个源文件都被 include 在 kernel/sched.c 中进行编译了。CFS Class 使用 sched_entity 结构作为调度实体,其中包含权重、运行时间等信息,比 RT Class 复杂,其中还有专门的红黑树。RT Class 使用 sched_rt_entity 作为调度实体。 每个 task_struct 中都包含了 sched_entity 和 sched_rt_entity 这两个字段,sched_class 中则有 enqueue_task、dequeue_task 等函数指针指向对应调度算法中的实现函数,enqueue_task 将进程加入运行队列,dequeue_task 将进程从队列中移除,由于这段变化较大而且比较复杂,有关这三种调度算法的具体实现以后再来介绍了。 task_struct 的 real_parent 字段指向创建该进程的进程(如果父进程已不存在则为 init 进程),parent 指向当前进程的父进程,children 为该进程子进程列表,sibling 为该进程的兄弟进程列表,group_leader 字段指向该进程的线程组长。与 ULK 不同的是,ULK 中 ptrace_children 为被调试器 trace 的该进程的子进程列表,2.6.34 中 ptraced 字段包含该进程原本的子进程和 ptrace attach 的目标进程,ptrace_list 改为 ptrace_entry。另外 2.6.34 kernel 中已经引入 namespace 的概念,获得进程组 ID 和会话期 ID 的方式也于 ULK 中的有不少区别。 kernel 中进程的 PID 散列表存在 pid_hash 中以加快根据 PID 搜索 task_struct 的速度,pidhash_init 函数初始化此 PID 散列表,由于 2.6.34 中已有 namespace,pid_hashfn 也由原来的一个参数变为两个参数(增加一个 ns 参数表示哪个 namespace)。Linux kernel 也增加了 pid 和 […]
Tag: Learning
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 结构存放 GDT table,而且提供 get_cpu_gdt_table 函数取得某个 CPU 的 GDT。cpu_gdt_descr 也已去掉,新增了 desc_ptr 结构存放每个 CPU 的 GDT 信息,cpu_gdt_descr 也改为 early_gdt_descr。 看下简单看下新的切换 GDT 的实现: load_gdt 最终调用 lgdt 汇编指令。 2、页式内存管理: Intel 从 80386 开始支持页式内存管理,页单元将线性地址翻译为物理地址。当 CR0 控制寄存器中的 PG 位置为 1 时,启动分页管理功能,为 0 时,禁止分页管理功能,并且把线性地址作物理地址使用。 32 位线性地址的高 10 位为页表目录的下标(指向页表),中间 10 位为页表的下标(指向页面),低 12 位为该地址在页面(通常大小为 4 KB)中的偏移量,这样的二层寻址设计主要为了减少页表本身所占用的内存,由于页表目录和页表都为 10 位,因此都最多包含 1024 个项。正在使用的页表目录的物理地址存在 cr3 控制寄存器中。 在 32 位大小的页表目录(页表)的结构中,其高 20 位为页表(页面)基地址的高 20 位,其它的 flag 中包含一个 Present 标志,如果该值为 1,表示指向的页面或者页表在内存中,如果为 0,页单元会将线性地址存在 cr2 控制寄存器中,并产生异常号 14: page fault。 页表目录结构中另外有一个 Page Size 标志(页表结构没有此标志),如果设为 1,则页面大小可以为 2MB 或者 4MB,这样可以跳过页表转换,将 cr4 寄存器的 PSE 标志启用即可启用大页面支持,此时 32 位线程地址由高 10 位页表目录下标和低 22 位的偏移量。 为满足寻址超过 4GB 的需求,Intel 从 Pentium Pro 处理器开始,将处理器的地址引脚数量由原来的 32 个提升为 36 个,处理器的寻址空间也从 4GB 增到 64GB,并增加 PAE 页面机制(设置 cr4 寄存器的 PAE 标志启用):64G内存可以划分为 2^24 个页面,页表中的基地址由 20 位增为 24 位,页表结构的大小由 32 位增为 64 位,增加 PDDT 表从而使用三层寻址设计来解释 32 位的线性地址等等。PAE 机制稍显复杂,而且由于仍然使用 32 位线性地址,因此对于应用程序来说,仍然无法使用超过 4GB 的地址空间,64GB 只是对于 kernel 而言的。 顺带说下不同的 64 位架构下的页面寻址级别,见下表,可以看到常用的 x86_64 架构只用了 48 位的线性地址空间,但也达到了 256TB 咯 ^_^ 3、硬件cache: 由于现在 CPU 速度太快,频率已经动辄多少 GHz,而相对的 DRAM 内存频率就慢很多,而且 DRAM 由于设计上电容存在不可避免的漏电原因,DRAM 的数据只能保持很短的时间,必须隔一段时间就刷新一次,不刷新的话会造成存储的信息丢失;而 […]
未能看到整个森林-编程学习中所犯的错误
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/fail-to-see-the-big-picture/ 备注:本文根据 pongba 大哥的这篇E文文章翻译并结合自己体会总结而来,pongba 的E文原文请猛击这里: http://blog.csdn.net/pongba/article/details/2143245 人类的一个普遍的天性是容易被自己感兴趣的东西所吸引。 不论是本文要说的编程学习还是日常事务都是这样,包括美女之类(哈哈),这似乎是一个难以打破的公理。人类自文明开始以来就对非凡的自然现象下的本质非常好奇,我们渴望理解,渴望知道原因。人类天性就是用来解决问题的,我们热衷于解决问题,热衷于发现问题的本质。不过悲哀的是,我们也是问题的主要创造者。 具体说到编程学习这一块,pongba 的原文中用 interesting(感兴趣) 和 mundane(平凡普通的)这两个词来区分编程学习中的两类知识。 我们最开始学习编程时用到的最经典的 hello world 就是 interesting 的一种,看到自己敲的一段字符能让计算机打印出来 hello world 确实能激发我们的兴趣。这就是所谓的 Under the Hood,这是一个在英文技术文章里经常见到的词,原意是钻进魔术师的帐篷,屏住呼吸,瞪大眼睛,把那些奇妙的魔法看个通透,让自己的理解和技艺获得巨幅的提升,在IT界里就是深入理解的意思。 你在学会设计程序和了解程序能正确运行的原因之后,接下来你会做什么?你会继续写程序,发现你所用的编程语言的越来越多的细节。你会越来越了解你用的编程语言,能知道该语言能方便的做些什么,哪些不能很方便的实现。接下来各种语言的窍门就开始进入你的脑海,注意这些窍门最吸引人的地方在于能让你做到本来做不到的事情,能让你荷尔蒙迸发,让你很 happy。 从程序设计语言的角度来看,我们热衷于解决的问题往往是我们自己创造的。例如,最近有一种争论关于设计模式是语言中缺失的一种特性。首先我们创造一门程序设计语言,由于一些设计时没有预料到的缺点,在使用中发现了,我们使用包括设计模式在内的一些语言窍门来解决它。但随着时间推移,我们就会发现这些模式不但没有价值,反而变成一种沉重的负担,这时通常会把这些作为新特性加入到语言中。 通常我们在解决以前的语言造成的问题过程中,我们通常又会造成新的问题。例如,现在总有 DSL 和 GPL 的争论,注意这里的 GPL 不是 GPL 开放授权协议,而是 Gerneral-purpose Language(通用语言),DSL 是 Domain-specific Language(领域专用语言)。DSL在很多人心目中是“非程序员的编程语言”,其首要目的是使程序尽可能接近领域中遇到的问题,消除不必要的间接性和复杂性,而其最终受众一般不是普通的程序员。一方面,将领域专用的一些特性加入到语言中,对那些需要对特定领域编程的人来说会非常便利;而另一方面,它会限制语言的使用范围。相对于DSL,GPL的最大优势在于可以为理论上无限的应用领域服务。GPL最大的妥协在于当面对领域相关的问题时,它只相当于一个 second-class language,这是为什么微软要搞一个CLR(通用语言运行时,Common Language Runtime)运行环境,也是为什么 Martin Fowler 要倡导面向语言编程LOP(Language-oriented Programming)了。 因此,在这总结一下,我们创造了各种语言抽象概念以使语言更加易用,但周尔反复的是我们总是在解决一个问题时创造时另一个新的问题。由于我们的程序设计语言都存在着不可避免的缺陷,这样语言窍门之类的东西就会登堂入室,并偷走我们的关注点(原文如此,嘿嘿),这也是为什么市面上有如此多的编程语言技巧书,语言陷阱介绍之类的,而且销量似乎都甚好。你可以看看任何C++编程学习的推荐书列表中,都不乏这样的例子。 然而到底是什么导致我们在编程学习时如在一堆树木里迷失,而导致没有看到整个森林?为了什么我们要学习这些奇淫技巧呢?实际上我们不是真正的需要,但我们内心里趋向于学习这些技巧,因为像文章开头说的,我们天生就是问题解决者,我们喜欢解决问题,即使这些问题是我们自己创造的。但这些奇淫技巧实际上只要在真正需要时按需学习即可,我们被这些东西吸引的原因在于: 1、我们喜欢新事物,如果什么东西是新事物就很有趣; 2、我们喜欢赶时髦(jump on the bandwagon)。 这就引出人类的第二个普遍的天性:赶时髦,如果所有人都做一件事,那我无论如何也得做。 不光是一些公司或者团体使用这个策略引诱我们去赶时髦,我们还热衷于创造自己的潮流。每当有新的语言或者技巧出来的时候,我们总是欢呼雀跃,总是被这些新带来的特性的光晕笼罩,而忘了它实际包含的问题,我们总是把它当做是万能灵药,开始万般饥渴地学习它。程序员是一种聪明的动物(原谅我如此直白,哈哈),不过有时显得过于聪明。他们总是渴望于新的事物(在任何编程论坛上找一圈就能得到验证,你会发现成千上万的程序语言技术细节的问题,学习这些东西是永无止境的,但程序员就是如此地欲罢不能),就像野兽永远不能停止对于食物的饥渴一样。 下面说说程序员普遍不爱的平凡的东西,什么是大多数程序员不喜欢的东西? 大多数程序员不爱的东西包括:编程原则,从小的编码规则(例如:永远给变量起一个有意义的名字)到大的项目设计原则(例如:在写代码之前先写测试文档)都有,还有文档的编制之类的,这些都是比较乏味的,不会显得古怪有味道,显得没有挑战性,显得没有那么酷。我们无法向外界展示遵从一些愚蠢的规则是多么聪明的一件事。我们尤其钟爱的是写一些疯狂的技巧代码或使用一些耀眼的模式以使别人都不知道我们在做什么(或者每个人都知道我们在做什么)。 接下来是人类的第三个天性:自私的偏见,我们热爱我们所做的,或者我们是谁,我们讨厌与之对抗或相反的东西。 不管你是否愿意承认这点,我们都有过这个体验。当我们对某些语言或平台非常熟悉时,我们就容易产生自私的偏见,它会影响我们想学习和不想学习的东西。你应该可以在一些论坛上感受到关于编程语言的争论是如此普遍。我们总是被蒙蔽了双眼,没有看到自己所用的语言或平台上的缺点和其它语言上的优点。我们限制了学习新的语言的能力,从某种意义上来说,我们限制了自己的潜能。 翻译的总结: 一方面,大多数时候我们学的东西有点太多了。我们像飞蛾扑火般被新事物吸引,我们经常是在学习周围的人在学习的,或者是别人告诉我们要学习的。但如果我们抓牢了一些本质的知识,其它的东西就完全可以按需学习。别再沉迷于技术技巧,除非它是必备的或者你马上就要用到的。因为要学习的技术技巧总是无穷无尽的,你应该将你的时间花在更有用的东西上(学些本质知识,学习编程思想,或者学习另一门编程语言)。 但另一方面,我们学的东西又太少了。我们总是忽视看起来乏味但又非常重要的东西,例如以下观点(可能很多人都有过): 测试? --- 就像做爱前戴套一样不爽; 重构? --- 为什么要做这种不能带来新功能也不酷的东西; 防御式编程? --- 对不起,我知道自己在写什么; API设计? --- 拜托,我在写这么华丽的代码的时候去考虑别人怎么使用我的代码也太TMD难了; 新语言? --- 你是说我现在用的这个不够好?没看到我能随意折腾这门语言来实现我想做的? 这是我首次完整翻译别人写的E文文章,也加了一些自己的观点,希望读者和我都能在未来看到编程学习中的整个森林 ^_^,有任何错误欢迎指正咯。 参考: 1、DSL领域专用语言: http://en.wikipedia.org/wiki/Domain-specific_programming_language 2、LOP面向语言编程: http://en.wikipedia.org/wiki/Language-oriented_programming