`
utensil
  • 浏览: 152419 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Linux 2.6内核笔记【Process-3:fork、内核进程】

阅读更多

Utensil按:

 

最后的几篇Linux内核笔记实在是太难产了,这中途读完了APUE,并以JavaEye闲聊的形式做了无数细小的笔记(不日将整理为博客);也第3次(还是第4次?)阅读了《ACE程序员指南》,不过这一次终于做下了笔记;也看完了Programming Erlang,用Erlang来写基于UDP的TCP的ErlyUbt已经渐渐现出眉目,也已push到了GitHub上面。可惜就是这段时间的该做的正事却没什么进展...

 

《Understanding Linux Kernel》在18号必须还给图书馆了...在这两天电脑坏了的日子里,第3次读了即将做笔记的中断与异常、内核同步、时间测量,其余的章节也略读完毕,这些章节希望能够写成一些细小的闲聊。预期电脑应该在今晚恢复正常,在这之前,我来到图书馆,开始写作这酝酿已久的笔记。 第一篇,是对Process的一个收尾。

 

Process的终止 

这不是本笔记关注的重点,只记下以下一点:

 

C库函数exit()调用exit_group()系统调用(做事的是do_group_exit()),这会终止整个线程组,而exit_group()会调用exit()系统调用(做事的是do_exit())来终止一个指定的线程。 

 

Process的诞生 

POSIX里,创建process需要fork(),古老的fork()是很汗的,它会完整复制父进程的所有资源。Linux则将fork细分为下面三种情况:

 

如果是fork一个正常进程,那么就用Copy-on-Write(CoW)技术,子进程先用着父进程的所有页,它企图修改某一页时,再复制那一页给它去改;

 

如果要的是线程(轻量级进程),那么就是大家共同享有原先那些资源,大家一条船;

 

还有就是vfork()所代表的情况:子进程创建出来后,父进程阻塞,这样老虎不在家,猴子当大王,子进程继续用原先的地址空间,直到它终止,或者执行新的程序,父进程就结束阻塞。

 

一个关于系统调用的准备知识:系统调用xyz()的函数名往往为sys_xyz(),下文对系统调用仅以sys_xyz()的形态表达。

 

clone()界面 

在Linux里,创建进程的总的界面是clone(),这个函数并没有定义在Linux内核源代码中,而是libc的一部分,它负责建立新进程的stack并调用sys_clone()。而sys_clone()里面实际干活的是do_fork(),而do_fork()做了许多前前后后的琐事,真正复制进程描述符和相关数据结构的是copy_process()。

 

clone()是这个样子的:clone(fn, arg, flags, child_stack, 其它我们不关心的参数)。

 

fn是新进程应执行的函数, arg是这个函数的参数。

 

flags的低字节指定新进程结束时发送给老进程的信号,通常为SIGCHLD,高字节则为clone_flag,clone_flag很重要,它决定了clone的行为。有趣的一些clone_flag包括(这些flag定义于<linux/ include/ linux/ sched.h >):

 

CLONE_VM(Virtual Memory):新老进程共享memory descriptor和所有Page Table;

CLONE_FS(File System);

CLONE_FILES;

CLONE_SIGHAND(Signal Handling):新老进程共享信号描述符(signal handler和现已blocked/pending的信号队列);

CLONE_PTRACE:用于Debugging;

CLONE_PARENT:老进程的real_parent登记为新进程的parent和real_parent;

CLONE_THREAD:新进程加入老进程的线程组;

CLONE_STOPPED:创建你,但你别运行。

 

child_stack则是新进程用户态stack的地址,要么共享老进程的,要么老进程应为新进程分配新的stack。

 

do_fork()探究  

书中说:fork()和vfork()只不过是建立在调用clone()基础上的wrapper函数(也在libc中),实际上:

 

asmlinkage int sys_fork(struct pt_regs regs)
{
        return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
        /* 略去用于把regs拆开成可以传递给do_fork的参数的代码 */
        return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
        return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

 

我一开始猜想,fork()和vfork()直接呼唤sys_fork()和sys_vfork()应该也没什么问题,但是,注意到这三个系统调用都只接受pt_regs这样仅包含寄存器的参数,显然clone()的工作中主要的部分是把它自身接受的参数转换成寄存器的值,事实上,clone还需要将fn和args压入stack,因为do_fork()是这样子的:

 

do_fork(clone_flags, stack_start, regs, 一些我们不关心的参数)

 

也就是说do_fork不了解也不需要知道fn和args,它做完fork之后,在某个return处,类似于之前在process切换用过的技巧(jmp+ret)将使CPU从stack中获取返回地址,并错误而正确地拿到了fn的地址。这正是clone()这个wrapper要做的事情,fork()和vfork()不妨复用clone()的辛苦。

 

do_fork()调用完copy_process之后,除非你指定CLONE_STOPPED,就会呼唤wake_up_new_task(),这里面有一点很有趣:

 

如果新老进程在同一CPU上运行,而且没有指定CLONE_VM(也就是终究要分家,要动用CoW),那么就会让新进程先于老进程运行,这样,如果新进程一上来就exec,就省去了CoW的功夫。

 

这是因为exec内部会调用flush_old_exec(),从与老进程的共享中中脱离,从此拥有自己的信号描述符、文件,释放了原先的mmap,消灭了对老进程的所有知识——这正是为什么成功执行的exec不会返回也无法返回。总之,此后再也没有共享,自然也不会需要CoW。(参见《Program Execution》一章《exec function》中的介绍。)

 

内核进程(Kernel thread) 

什么是书中所说的“内核线程”?首先要说明,由于Linux内核中对process和thread的混用,这里的thread其实完全可以理解为process,等价于普通的进程,不能理解为老进程中的一个属于内核的线程。因此,下文都称之为内核进程。

 

内核进程是会和其他进城一样被调度的实体,它和进程的唯一区别就是,它永远运行于内核态,也只访问属于内核的那一部分线性地址(大于PAGE_OFFSET的)。

 

这就使得创建它的时候非常省事,直接和创建它的普通进程共享小于PAGE_OFFSE的线性地址,反正它也不用:

 

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
        /* 略去用于设置regs的代码 */
        return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}

 <linux/ include/ linux/ sched.h >中甚至定义了

 

#define CLONE_KERNEL     (CLONE_FS | CLONE_FILES | CLONE_SIGHAND )

 


可供kernel_thread()调用的时候使用,这样节省的克隆就更多了。

 

内核进程由于不受不必要的用户态上下文拖累,可以用于执行一些重要的内核任务,比如,刷新磁盘高速缓存,交换出不用的pageframe,服务网络连接等等,这些任务以前是周期性执行的进程,是线性的执行方式,现在的内核把用户态从他们身上剥离,并且和其它进程放到一起来调度,能获得更好的响应表现。

 

所有进程的祖先是进程0,称为idle进程或swapper进程,它是内核初始化时创建的一个内核进程,它初始化一堆数据结构之后会创建init进程,执行init()函数,其中调用exec执行了init程序,至此,init进程变成了一个普通进程。而idle进程之后则一直执行cpu_idle()函数没事干。调度程序只有在没有进程处于可运行状态(TASK_RUNNING)才会选择它。

 

如果有多个CPU,BIOS一开始会禁用其它CPU,只留一个,进程0就在其上诞生,它会激活其它CPU,并通过copy_process让每个CPU都有一个pid为0的进程,从而形成了每个CPU都有一个0进程的局面。

0
0
分享到:
评论

相关推荐

    fork-wrapper:生成一个程序,然后等待所有子进程终止

    叉式包装机fork-wrapper产生一个程序并在退出之前等待所有子进程退出。 fork-wrapper将以最后一个要退出的子进程的退出状态终止。问题许多守护进程(例如 Unicorn)通过创建一个新的主进程来实现热重载。 一旦新的主...

    Linux 内核2.6 + ssh

    1. **Linux内核2.6**:Linux内核是开源的操作系统核心,版本2.6是一个重要的里程碑,它引入了许多改进和优化,包括更好的内存管理、增强的文件系统支持、多处理器(SMP)性能提升以及对硬件的更广泛支持。...

    Linux2.6内核实现的是NPTL线程模型

    在Linux2.6内核中,依然采用进程来模拟线程的行为,但新引入了线程组(进程组)的概念。线程组是由一个或多个线程组成的集合,它们共享相同的线程组ID(TID)。在2.4内核之前,每个线程都被视为一个独立的进程,并...

    LINUX内核编程---相当经典

    3. **进程管理**:Linux内核负责进程的创建、调度、同步和通信。理解`fork()`和`exec()`如何创建新进程,以及`wait()`和`signal()`如何处理进程间交互,对于编写多线程和多进程程序至关重要。 4. **内存管理**:...

    linux操作系统教程2.6内核中科大陈香兰

    《Linux操作系统教程2.6内核——中科大陈香兰》是一份深入讲解Linux操作系统的珍贵资料,由著名教育家陈香兰老师编撰。这份讲义覆盖了操作系统的核心概念和关键技术,尤其针对Linux 2.6内核进行了详尽阐述。以下是...

    创建新进程:fork函数:fork函数干什么? fork函数与vfork函数的区别在哪里?为何在一个fork的子进程分支中使用_exit函数而不使用exit函数?

    创建新进程:fork 函数 fork 函数是 UNIX 统操作系统中用于创建新进程的系统调用。它创建了一个完全相同的子进程副本,并返回一个进程标识符(PID)。fork 函数的返回值在父进程和子进程中不同:在父进程中,返回子...

    Linux内核设计与实现_第三版英文版

    - **模块化设计**:Linux内核采用了高度模块化的设计,使得不同功能可以通过加载或卸载模块来实现动态扩展。 - **进程管理**:包括进程创建、调度、同步和通信机制。 - **内存管理**:实现虚拟内存机制,支持分页和...

    linux-2.6.21.5源代码

    Linux 2.6.21.5提供了大量的系统调用,如open、read、write、fork、execve等,它们提供了基本的文件操作、进程管理、内存管理等功能。 八、并发与同步 在多处理器系统中,Linux 2.6.21.5内核提供了线程、信号量、...

    linux 内核精髓-精通linux内核必会的75个绝技

    3. **内存管理**:探讨Linux内核如何分配、释放和管理物理及虚拟内存,包括页面缓存、内存映射、交换机制等,这对优化系统性能至关重要。 4. **文件系统**:详述Linux下的VFS(虚拟文件系统)和具体文件系统的实现...

    child-process-manager:此模块用于管理节点的子进程数量,以便它不会像每次需要一些 CPU 密集型处理时 fork 子进程时那样消耗所有 CPU

    此模块用于管理节点的子进程数量,以便它不会像每次需要一些 CPU 密集型处理时分叉子进程时那样消耗所有 CPU。 您只需要指定以下配置值: setMaxCount :这是您的服务器可以承受的最大子模块数。 您可以通过试用来...

    基于Linux内核2.6的进程拦截机制的研究和实现.pdf

    【基于Linux内核2.6的进程拦截机制】 在Linux操作系统中,进程是系统执行任务的基本单位。Linux内核2.6版本引入了一系列先进的进程管理机制,这些机制使得内核更加稳定、高效。本文主要探讨如何在Linux内核2.6的...

    linux-api-2.6.22.rar_linux_linux api_linux-2.6.22.6

    8. **内核模块**:Linux 2.6.22.6支持可加载内核模块(LKM),开发者可以使用`init_module()`和`cleanup_module()`来加载和卸载模块,实现动态扩展内核功能。 通过对《Linux内核API 2.6.22.6》的深入学习,开发者...

    嵌入式Linux高级班

    - 调度算法:了解Linux内核中使用的进程调度算法。 - 进程状态:掌握进程的不同状态及其转换规则。 **4.5 系统调用** - 调用机制:理解系统调用的工作原理。 - 实现案例:学习如何在内核中实现新的系统调用。 **...

    linux系统调用.pdf

    在Linux系统中,系统调用是应用程序请求内核服务的唯一方式。本文档涉及了Linux系统调用的广泛内容,包括进程控制、文件操作、网络通信、信号处理、IPC(进程间通信)等。 1. 进程控制 系统调用在进程控制方面允许...

    Linux内核阅读笔记

    Linux内核阅读笔记 本文是 Linux 内核 0.11 源代码的完全注释,旨在帮助读者了解 Linux 内核的工作原理和实现细节。下面是从标题、描述、标签和部分内容中提取的相关知识点: 1. 中断机制:中断是 OS 的主线,...

    第2部分第6次课-linux2.6内核之系统调用及定时器..ppt

    【嵌入式Linux内核体系架构】中的【系统调用】和【定时器】是Linux内核中的核心组件,它们对于系统的运行至关重要。系统调用是用户空间与内核空间交互的主要桥梁,允许用户程序安全地访问操作系统提供的各种服务。 ...

    LInux内核精髓-精通Linux内核必会的75个绝技.pdf

    3. **文件系统**:Linux内核支持多种文件系统,如EXT4、XFS、Btrfs等。文件系统的挂载、卸载、I/O操作及缓存机制是其中的关键点。 4. **设备驱动**:了解设备驱动程序如何与硬件交互,包括字符设备、块设备驱动的...

    linux-insides-zh:​​Linux内核揭秘

    - 进程与线程:Linux内核中的进程模型,如何创建、调度和销毁进程,以及线程在内核中的实现和调度策略。 2. **内核启动过程** - 引导加载器:如GRUB,如何将Linux内核加载到内存并开始执行。 - 内核初始化:从...

    Linux system call quick reference

    根据提供的文件信息,我们可以深入探讨Linux系统调用的相关知识点,包括其定义、使用方式以及一些常用的系统调用功能。 ### Linux系统调用简介 在Linux操作系统中,**系统调用**是内核向用户空间程序提供的一系列...

Global site tag (gtag.js) - Google Analytics