精华内容
下载资源
问答
  • Linux 进程管理

    千次阅读 2015-04-21 12:23:47
     在Linux的内核的五大组成模块中,进程管理模块时非常重要的一部分,它虽然不像内存管理、虚拟文件系统等模块那样复杂,也不像进程间通信模块那样条理化,但作为五大内核模块之一,进程管理对我们理解内核的运作、...

    引言:


           在Linux的内核的五大组成模块中,进程管理模块时非常重要的一部分,它虽然不像内存管理、虚拟文件系统等模块那样复杂,也不像进程间通信模块那样条理化,但作为五大内核模块之一,进程管理对我们理解内核的运作、对于我们以后的编程非常重要。同时,作为五大组成模块中的核心模块,它与其他四个模块都有联系。下面就对进程模块进行想写的介绍,首先要了解进程及其相关的概念。其次介绍进程的创建、切换、撤销等基本操作。除此之外,还给出了Linux内核是如何对进程进行调度管理的。


         一、进程及其相关概念


           进程:进程可以理解为程序执行的一个实例,它包括可执行程序以及与其相关的系统资源,比如打开的文件、挂起的信号、内核内部数据、处理器状态、内存地址空间及包含全局变量的数据段等。从内核的角度看,进程也可以称为任务。

           进程描述符:与进程相关的事情非常多,比如进程的状态、进程的优先级、进程的地址空间、允许该进程访问的文件等等,Linux内核为此专门设计了一个类型为task_struct的结构体,称之为进程描述符。进程描述符中包含了内核管理进程的所有信息,可以说,只要得到一个进程的进程描述符,就可以知道一个进程的所有信息。

           进程状态:进程描述符task_struct结构体中有一个state字段,表示进程当前的所处状态。从进程的创建到进程的删除,它可以经过5种不同的状态,分别是可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、跟踪状态。除此之外,当进程被终止时,还可能会变为僵死状态、僵死撤消状态。内核可以使用宏set_current_state(state)设置当前进程的状态,用set_task_state(task,state)设置某进程的状态。

           进程标示符:进程描述task_struct结构体中的pid字段可以标识唯一标识一个进程,称之为进程标识符PID。当创建一个新进程时,PID是按照顺序从小到大分配给新进程的。内核通过管理一个pidmap_array位图来表示当前已分配的PID和闲置的PID号。注意:在多线程组中,所有的线程共享相同的PID。除了进程标识符外,内核对进程的大部分访问时通过进程描述符指针进行的。

           进程关系:进程之间的关系有亲属关系和非亲属关系。亲属关系包括父子关系和兄弟关系等。其中由tast_struct结构体中的parent/children/real_parent/sibling等字段描述。除了亲属关系外,还有其他关系,比如,一个进程是一个进程组或登录会话的领头进程,可能是一个线程组的领头进程,这些关系由group_leader/tgid/signal->pgrp等字段描述。

           进程资源:为了防止进程过度的使用系统资源,内核为每个进程使用资源的数量进行了一些限制。其中包括进程地址空间的最大数、进程使用CPU的最大时间、堆的最大值、文件大小的最大值、文件锁数量的最大值、消息队列的最大字节数、打开文件描述符的最大数、进程拥有的页框最大数等。


           二、进程的创建、切换、撤销

           

           进程的创建:在Linux环境编程时,一般采用fork()函数来创建新的进程,当然,那是在用户空间的函数,它会调用内核中的clone()系统调用,由clone()函数继续调用do_fork()完成进程的创建。

           传统Unix系统中,创建的子进程复制父进程所拥有的资源,这种方法效率低,因为子进程需要拷贝父进程的整个地址空间。但是,子进程几乎不必读或修改父进程拥有的所有资源,因为很多情况下,子进程创建后会立即调用exec()一族的函数,并清除父进程仔细拷贝过来的地址空间。现代Unix系统用三种方式解决了这个问题:1、写实复制技术允许父子进程读相同的物理页。2、轻量级进程允许父子进程共享每进程在内核的很多数据结构。3、vfork()系统调用创建的进程能共享父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。整个进程创建过程可能涉及到如下函数:

           fork()/vfork()/_clone----------->clone()--------->do_fork()---------->copy_process()

           上面的创建过程结束之后,就有了处于可运行状态的完整的子进程,新的子进程有了PID、进程描述符等各种数据结构,要想实际运行它,还需要调度程序把CPU交给新创建的子进程。


          除了进程外,还有内核线程(用kernet_thread创建)的概念。在Linux中,内核线程与普通进程存在以下两个方面的不同:

           1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可运行在用户态。

           2、因为内核线程只运行在内核态,它只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。


           撤销进程:进程终止后,需要通知内核以便内核释放进程所拥有的资源,包括内存、打开文件以及其他资源,如信号量。进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。

           除了进程自己终止自己外,内核可以有选择地强迫整个线程组死掉。这发生在:当进程接收到一个不能处理或忽视的信号时,或者当内核正在代表进程运行时再内核态产生一个不可恢复的CPU异常时。

            有两个终止用户态应用的系统调用:exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。exit()系统调用,它终止一个线程,而不管该线程所属线程组中的所有其他进程。do_exit()是实现这个系统调用的主要内核函数。


           进程切换:进程切换又称为任务切换、上下文切换。它是这样一种行为,为了控制进程的执行,内核挂起当前在CPU上运行的进程,并恢复以前挂起的某个进程的执行。

            跟函数的调用类似,进程切换时,一般要在CPU上装载要执行进程的进程上下文。进程的硬件上下文指:可执行程序上下文的一个子集,是进程恢复执行前装入寄存器的一组数据。其中一部分放在TSS段,即任务状态段,剩余部分存放在内核态堆栈中。进程的切换只发生在内核态,在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。

            进程的切换有两种方法,一种是硬件切换,一种是软件切换。软件切换就是利用程序逐步执行切换,它的优点是,可以对切换时装入的数据进行合法性检查,执行时间虽与硬件切换大致相同,但仍有可改进的地方。


         进程切换使用schedule()函数完成,在本质上,每个进程切换由两部分组成:1、切换页全局目录以安装一个新的地址空间。2、切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器,主要有switch_to函数完成。


         三、进程调度


           调度策略:调度策略就是这样一组规则:决定什么时候以怎样的方式选择一个新进程运行的规则。Linux的调度基于分时技术:多个进程以“时间多路复用”方式运行,因为CPU的时间被分成“片”,给每个可运行进程分配一片。调度策略也是根据进程的优先级对它们进行分类。在Linux中,进程的优先级是动态的。调度程序跟踪进程正在做什么,并周期性地调整它们的优先级。根据不同的分类标准,可以把进程分成不同的类型。比如可以把一个进程看作是“I/O受限”或“CPU受限”。也可把进程区分为以下三类:交互式进程、批处理进程、实时进程。Linux的进程是抢占式的,无论是处于内核态还是用户态。时间片的长短对系统性能是很关键的:它既不能太长也不能太短。如果平均时间片太短,由进程切换引起的系统额外开销就变得非常高。如果平均时间片太长,进程看起来就不再是并发执行的。对时间片大小的选择始终是一种折中。Linux采用单凭经验的方法,即选择尽可能长、同时能保持良好响应时间的一个时间片。


           调度算法:早起的Linux中,调度算法是根据进程的优先级选择“最佳”进程来执行,它的缺点是时间开销与“可运行进程数量”有关。现代的Linux中,调度算法可以在固定时间内(与可运行进程数量无关)选中要运行的进程。首先,我们必须知道进程可以分为实时进程与普通进程。每个LInux进程总是按照如下的调度类型被调度:先进先出的实时进程、时间片轮转的实时进程、普通的分时进程。调度算法根据进程是普通进程还是实时进程而有很大不同。

           普通进程的调度:每个普通进程都有它自己的静态优先级(值是从100到139),调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间调度的程度。静态优先级决定进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间片长度。普通进程除了静态优先级,还有动态优先级。动态优先级是调度程序在选择新进程来运行的时候使用的数。平均睡眠时间是进程在睡眠状态所消耗的平均纳秒数。即使具有较高静态优先级的普通进程获得了较大的CPU时间片,也不应该使静态优先级较低的进程无法运行。为了避免这个问题,提出了活动进程和过期进程的概念,活动进程指进程的时间片还未用完,过期进程指进程的时间片以用完,即使过期进程的优先级更高,也不能继续运行,除非等到所有活动进程都过期以后。

          实时进程的调度:每个实时进程都与一个试试优先级相关,实时优先级是一个范围从1到99的值。跟普通进程不同,实时进程总是被当作活动进程。


          调度程序所使用的主要数据结构:数据结构runqueue和进程描述符

           数据结构runqueue:runqueue数据结构中最重要的字段是与可运行进程 的链表相关的字段。其中的arrays字段是活动进程和过期进程的两个集合,active字段是指向活动进程链表的指针,expired字段是指向过期进程链表的指针。

           进程描述符:每个进程描述符都包括几个与调度相关的字段。其中的time_slice字段是在进程的时间片中还剩余的时钟节拍数。它由copy_process函数设置:父进程的剩余节拍数被划分为两等分,一份给父进程,一份给子进程。


         四、调度程序所使用的函数


           调度程序依靠几个函数来完成调度工作,其中最重要的函数如下:

            try_to_wake_up()函数通过把进程状态设置为TASK_RUNNING,并把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程。

            recalc_task_prio()函数更新进程的平均睡眠时间和动态优先级。

            schedule()憾事实现调度程序,它的任务时从运行队列的链表中找到一个进程,并随后将CPU分配给这个进程。schedule()可以由几个内核控制路径调用,可以采用直接调用或延迟调用的方式


           总结:

           Linux内核中的进程管理模块非常重要,它是连接其他4大模块的重要桥梁,它也非常复杂,理解它的一些基本原理,对于理解Linux内核非常重要,上面只是对它进行了一些简单的描述,并没有深入到具体实现细节,希望已有有机会能深入分析实现的细节。


    转自:懒人李冰

    展开全文
  • linux进程管理

    万次阅读 2016-01-14 11:47:07
    因此,对进程及其调度进行一般管理就显得极为重要。  在用户空间,进程是由进程标识符(PID)表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会

         Linux 是一种动态系统,能够适应不断变化的计算需求。Linux 计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程及其调度进行一般管理就显得极为重要。
         在用户空间,进程是由进程标识符(PID)表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会更改,但 PID 可以在进程销毁后被重新使用,所以对它们进行缓存并不见得总是理想的。在用户空间,创建进程可以采用几种方式。可以执行一个程序(这会导致新进程的创建),也可以在程序内,调用一个forkexec系统调用。fork调用会导致创建一个子进程,而exec调用则会用新程序代替当前进程上下文。这里将对这几种方法进行讨论以便您能很好地理解它们的工作原理。
        这里将按照下面的顺序展开对进程的介绍,首先展示进程的内核表示以及它们是如何在内核内被管理的,然后来看看进程创建和调度的各种方式(在一个或多个处理器上),最后介绍进程的销毁。内核的版本为2.6.32.45。


    1. 进程描述符

         在 Linux 内核内,进程是由相当大的一个称为 task_struct 的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(如父和子)。task_struct 位于 ./linux/include/linux/sched.h(注意./linux/指向内核源代码树)。下面是task_struct结构:

      struct task_struct {  
           volatile long state;    /* -1 不可运行, 0 可运行, >0 已停止 */  
           void *stack;            /* 堆栈 */  
           atomic_t usage;  
           unsigned int flags; /* 一组标志 */  
           unsigned int ptrace;  
           /* ...  */  
         
           int prio, static_prio, normal_prio;  /* 优先级  */  
          /* ...  */  
        
          struct list_head tasks;  /* 执行的线程(可以有很多)  */  
          struct plist_node pushable_tasks;  
        
          struct mm_struct *mm, *active_mm;   /* 内存页(进程地址空间)  */  
        
          /* 进行状态 */  
          int exit_state;  
          int exit_code, exit_signal;  
          int pdeath_signal;  /*  当父进程死亡时要发送的信号  */  
          <span style="font-family: Arial, Helvetica, sans-serif;">/* ... */     </span>
        
          pid_t pid;  /* 进程号 */  
          pid_t tgid;  
       
          /* ... */  
          struct task_struct *real_parent; /* 实际父进程real parent process */  
          struct task_struct *parent; /* SIGCHLD的接受者,由wait4()报告 */  
          struct list_head children;  /* 子进程列表 */  
          struct list_head sibling;   /* 兄弟进程列表 */  
          struct task_struct *group_leader;   /* 线程组的leader */  
          /* ... */  
        
          char comm[TASK_COMM_LEN]; /* 可执行程序的名称(不包含路径) */  
          /* 文件系统信息 */  
          int link_count, total_link_count;  
          /* ... */  
        
          /* 特定CPU架构的状态 */  
          struct thread_struct thread;  
         /* 进程当前所在的目录描述 */  
           struct fs_struct *fs;  
          /* 打开的文件描述信息 */  
          struct files_struct *files;  
          /* ... */  
      };  

          在task_struct结构中,可以看到几个预料之中的项,比如执行的状态、堆栈、一组标志、父进程、执行的线程(可以有很多)以及开放文件。state 变量是一些表明任务状态的比特位。最常见的状态有:TASK_RUNNING 表示进程正在运行,或是排在运行队列中正要运行;TASK_INTERRUPTIBLE 表示进程正在休眠、TASK_UNINTERRUPTIBLE表示进程正在休眠但不能叫醒;TASK_STOPPED 表示进程停止等等。这些标志的完整列表可以在 ./linux/include/linux/sched.h 内找到。
          flags 定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用 comm(命令)字段。每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。tasks字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个 next 指针(指向下一个任务)。
        进程的地址空间由mm 和 active_mm 字段表示。mm代表的是进程的内存描述符,而 active_mm 则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。thread_struct thread结构则用来标识进程的存储状态,此元素依赖于Linux在其上运行的特定架构。例如对于x86架构,在./linux/arch/x86/include/asm/processor.h的thread_struct结构中可以找到该进程自执行上下文切换后的存储(硬件注册表、程序计数器等)。代码如下:

    struct thread_struct {
    	/* Cached TLS descriptors: */
    	struct desc_struct	tls_array[GDT_ENTRY_TLS_ENTRIES];
    	unsigned long		sp0;
    	unsigned long		sp;
    #ifdef CONFIG_X86_32
    	unsigned long		sysenter_cs;
    #else
    	unsigned long		usersp;	/* Copy from PDA */
    	unsigned short		es;
    	unsigned short		ds;
    	unsigned short		fsindex;
    	unsigned short		gsindex;
    #endif
    #ifdef CONFIG_X86_32
    	unsigned long		ip;
    #endif
    	/* ... */
    #ifdef CONFIG_X86_32
    	/* Virtual 86 mode info */
    	struct vm86_struct __user *vm86_info;
    	unsigned long		screen_bitmap;
    	unsigned long		v86flags;
    	unsigned long		v86mask;
    	unsigned long		saved_sp0;
    	unsigned int		saved_fs;
    	unsigned int		saved_gs;
    #endif
    	/* IO permissions: */
    	unsigned long		*io_bitmap_ptr;
    	unsigned long		iopl;
    	/* Max allowed port in the bitmap, in bytes: */
    	unsigned		io_bitmap_max;
    /* MSR_IA32_DEBUGCTLMSR value to switch in if TIF_DEBUGCTLMSR is set.  */
    	unsigned long	debugctlmsr;
    	/* Debug Store context; see asm/ds.h */
    	struct ds_context	*ds_ctx;
    };

        2. 进程管理
        在很多情况下,进程都是动态创建并由一个动态分配的 task_struct 表示。一个例外是init 进程本身,它总是存在并由一个静态分配的task_struct表示,参看./linux/arch/x86/kernel/init_task.c,代码如下:

    static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
    static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);
    
    /*
     * 初始化线程结构
     */
    union thread_union init_thread_union __init_task_data =
    	{ INIT_THREAD_INFO(init_task) };
    
    /*
     * 初始化init进程的结构。所有其他进程的结构将由fork.c中的slabs来分配
     */
    struct task_struct init_task = INIT_TASK(init_task);
    EXPORT_SYMBOL(init_task);
    
    /*
     * per-CPU TSS segments. 
     */
    DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;

          注意进程虽然都是动态分配的,但还是需要考虑最大进程数。在内核内最大进程数是由一个称为max_threads的符号表示的,它可以在 ./linux/kernel/fork.c 内找到。可以通过/proc/sys/kernel/threads-max 的 proc 文件系统从用户空间更改此值。
          Linux 内所有进程的分配有两种方式。第一种方式是通过一个哈希表,由PID 值进行哈希计算得到;第二种方式是通过双链循环表。循环表非常适合于对任务列表进行迭代。由于列表是循环的,没有头或尾;但是由于 init_task 总是存在,所以可以将其用作继续向前迭代的一个锚点。让我们来看一个遍历当前任务集的例子。任务列表无法从用户空间访问,但该问题很容易解决,方法是以模块形式向内核内插入代码。下面给出一个很简单的程序,它会迭代任务列表并会提供有关每个任务的少量信息(name、pid和 parent 名)。注意,在这里,此模块使用 printk 来发出结果。要查看具体的结果,可以通过 cat 实用工具(或实时的 tail -f/var/log/messages)查看 /var/log/messages 文件。next_task函数是 sched.h 内的一个宏,它简化了任务列表的迭代(返回下一个任务的 task_struct 引用)。如下:

    #define next_task(p) \
    	list_entry_rcu((p)->tasks.next, struct task_struct, tasks)
    查询任务列表信息的简单内核模块:
    <pre name="code" class="cpp">#include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/sched.h>
    
    int init_module(void)
    {
    	/* Set up the anchor point */
    	struct task_struct *task=&init_task;
    	/* Walk through the task list, until we hit the init_task again */
    	do {
    		printk(KERN_INFO "=== %s [%d] parent %s\n",
    			task->comm,task->pid,task->parent->comm);
    	} while((task=next_task(task))!=&init_task);
    	
    	printk(KERN_INFO "Current task is %s [%d]\n", current->comm,current->pid);
    	return 0;
    }
    
    void cleanup_module(void)
    {
    	return;
    }

     
     

        编译此模块的Makefile文件如下:

    obj-m += procsview.o
    
    KDIR := /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
    
    default:
    	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

        在编译后,可以用insmod procsview.ko 插入模块对象,也可以用 rmmod procsview 删除它。插入后,/var/log/messages可显示输出,如下所示。从中可以看到,这里有一个空闲任务(称为 swapper)和init 任务(pid 1)。
    Dec 28 23:18:16 ubuntu kernel: [12128.910863]=== swapper [0] parent swapper
    Dec 28 23:18:16 ubuntu kernel: [12128.910934]=== init [1] parent swapper
    Dec 28 23:18:16 ubuntu kernel: [12128.910945]=== kthreadd [2] parent swapper
    Dec 28 23:18:16 ubuntu kernel: [12128.910953]=== migration/0 [3] parent kthreadd
    ......
    Dec 28 23:24:12 ubuntu kernel: [12485.295015]Current task is insmod [6051]
        Linux 维护一个称为current的宏,标识当前正在运行的进程(类型是 task_struct)。模块尾部的那行prink用于输出当前进程的运行命令及进程号。注意到当前的任务是 insmod,这是因为 init_module 函数是在insmod 命令执行的上下文运行的。current 符号实际指的是一个函数(get_current),可在一个与 arch 有关的头部中找到它。比如 ./linux/arch/x86/include/asm/current.h,如下:

    #include <linux/compiler.h>
    #include <asm/percpu.h>
    
    #ifndef __ASSEMBLY__
    struct task_struct;
    
    DECLARE_PER_CPU(struct task_struct *, current_task);
    
    static __always_inline struct task_struct *get_current(void)
    {
    	return percpu_read_stable(current_task);
    }
    
    #define current get_current()
    
    #endif /* __ASSEMBLY__ */
    
    #endif /* _ASM_X86_CURRENT_H */

    3. 进程创建
          用户空间内可以通过执行一个程序、或者在程序内调用fork(或exec)系统调用来创建进程,fork调用会导致创建一个子进程,而exec调用则会用新程序代替当前进程上下文。一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个用户态函数均由libc库提供,它们分别会调用Linux内核提供的同名系统调用fork,vfork和clone。下面以fork系统调用为例来介绍。
          传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。为了解决这个问题,内核中提供了上述三种不同的系统调用。
        (1)内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。
        (2)使用vfork函数创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项。父子进程任意一方对任何数据的修改使得另一方都可以感知到。为了使得双方不受这种影响,vfork函数创建了子进程后,父进程便被阻塞直至子进程调用了exec()exit()。由于现在fork函数引入了写时复制技术,在不考虑复制父进程页表项的情况下,vfork函数几乎不会被使用。
        (3)clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的clone标志参数来选择性的复制父进程的资源。
         大部分系统调用对应的例程都被命名为 sys_* 并提供某些初始功能以实现调用(例如错误检查或用户空间的行为),实际的工作常常会委派给另外一个名为 do_* 的函数。    

    在./linux/include/asm-generic/unistd.h中记录了所有的系统调用号及名称。注意fork实现与体系结构相关,对32位的x86系统会使用./linux/arch/x86/include/asm/unistd_32.h中的定义,fork系统调用编号为2。fork系统调用在unistd.h中的宏关联如下:

    #define __NR_fork 1079
    #ifdef CONFIG_MMU
    __SYSCALL(__NR_fork, sys_fork)
    #else
    __SYSCALL(__NR_fork, sys_ni_syscall)
    #endif

    在unistd_32.h中的调用号关联为:     #define __NR_fork      2    

    在很多情况下,用户空间任务和内核任务的底层机制是一致的。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。它们最终都会依赖于一个名为do_fork 的函数来创建新进程。例如在创建内核线程时,内核会调用一个名为 kernel_thread 的函数(对32位系统

    参见./linux/arch/x86/kernel/process_32.c,注意process.c是包含32/64bit都适用的代码,process_32.c

    是特定于32位架构,process_64.c是特定于64位架构),此函数执行某些初始化后会调用 do_fork。创

    建用户空间进程的情况与此类似。在用户空间,一个程序会调用fork,通过int $0x80之类的软中断会导致

    对名为sys_fork的内核函数的系统调用(参见 ./linux/arch/x86/kernel/process_32.c),如下:

    int sys_fork(struct pt_regs *regs)
    {
    	return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
    }

        最终都是直接调用do_fork。进程创建的函数层次结构如下图:
     

                                          图1 进程创建的函数层次结构

          从图中可以看到 do_fork 是进程创建的基础。可以在 ./linux/kernel/fork.c 内找到 do_fork 函数(以及合作函数 copy_process)。
          当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0×80汇编命令和执行sysenter汇编命令。后者是Intel在Pentium II中引入的指令,内核从2.6版本开始支持这条命令。这里将集中讨论以int $0×80方式进入系统调用的过程。
          通过int $0×80方式调用系统调用实际上是用户进程产生一个中断向量号为0×80的软中断。当用户态fork()调用发生时,用户态进程会保存调用号以及参数,然后发出int $0×80指令,陷入0x80中断。CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0×80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。32位x86系统的系统调用处理程序在./linux/arch/x86/kernel/entry_32.S中,代码如下:

    .macro SAVE_ALL
    	cld
    	PUSH_GS
    	pushl %fs
    	CFI_ADJUST_CFA_OFFSET 4
    	/*CFI_REL_OFFSET fs, 0;*/
    	pushl %es
    	CFI_ADJUST_CFA_OFFSET 4
    	/*CFI_REL_OFFSET es, 0;*/
    	pushl %ds
    	CFI_ADJUST_CFA_OFFSET 4
    	/*CFI_REL_OFFSET ds, 0;*/
    	pushl %eax
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET eax, 0
    	pushl %ebp
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET ebp, 0
    	pushl %edi
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET edi, 0
    	pushl %esi
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET esi, 0
    	pushl %edx
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET edx, 0
    	pushl %ecx
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET ecx, 0
    	pushl %ebx
    	CFI_ADJUST_CFA_OFFSET 4
    	CFI_REL_OFFSET ebx, 0
    	movl $(__USER_DS), %edx
    	movl %edx, %ds
    	movl %edx, %es
    	movl $(__KERNEL_PERCPU), %edx
    	movl %edx, %fs
    	SET_KERNEL_GS %edx
    .endm
    /* ... */
    ENTRY(system_call)
    	RING0_INT_FRAME			# 无论如何不能进入用户空间
    	pushl %eax			# 将保存的系统调用编号压入栈中
    	CFI_ADJUST_CFA_OFFSET 4
    	SAVE_ALL
    	GET_THREAD_INFO(%ebp)
    					# 检测进程是否被跟踪
    	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    	jnz syscall_trace_entry
    	cmpl $(nr_syscalls), %eax
    	jae syscall_badsys
    syscall_call:
    	call *sys_call_table(,%eax,4)	# 跳入对应服务例程
    	movl %eax,PT_EAX(%esp)		# 保存进程的返回值
    syscall_exit:
    	LOCKDEP_SYS_EXIT
    	DISABLE_INTERRUPTS(CLBR_ANY)	# 不要忘了在中断返回前关闭中断
    	TRACE_IRQS_OFF
    	movl TI_flags(%ebp), %ecx
    	testl $_TIF_ALLWORK_MASK, %ecx	# current->work
    	jne syscall_exit_work
    
    restore_all:
    	TRACE_IRQS_IRET
    restore_all_notrace:
    	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS, SS and CS
    	# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
    	# are returning to the kernel.
    	# See comments in process.c:copy_thread() for details.
    	movb PT_OLDSS(%esp), %ah
    	movb PT_CS(%esp), %al
    	andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
    	cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
    	CFI_REMEMBER_STATE
    	je ldt_ss			# returning to user-space with LDT SS
    restore_nocheck:
    	RESTORE_REGS 4			# skip orig_eax/error_code
    	CFI_ADJUST_CFA_OFFSET -4
    irq_return:
    	INTERRUPT_RETURN
    .section .fixup,"ax"

        分析:
        (1)在system_call函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在 system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。
        (2)通过GET_THREAD_INFO宏获得当前进程的thread_inof结构的地址;再检测当前进程是否被其他进程所跟踪(例如调试一个程序时,被调试的程序就处于被跟踪状态),也就是thread_info结构中flag字段的_TIF_ALLWORK_MASK被置1。如果发生被跟踪的情况则转向syscall_trace_entry标记的处理命令处。
        (3)对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳入到syscall_badsys标记的命令处。
        (4)如果系统调用好合法,则根据系统调用号查找./linux/arch/x86/kernel/syscall_table_32.S中的系统调用表sys_call_table,找到相应的函数入口点,跳入sys_fork这个服务例程当中。由于 sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。syscall_table_32.S中的代码如下:

    ENTRY(sys_call_table)
    	.long sys_restart_syscall	/* 0 - old "setup()" system call, used for restarting */
    	.long sys_exit
    	.long ptregs_fork
    	.long sys_read
    	.long sys_write
    	.long sys_open		/* 5 */
    	.long sys_close
    	/* ... */

       sys_call_table是系统调用多路分解表,使用 eax 中提供的索引来确定要调用该表中的哪个系统调用。
        (5)当系统调用服务例程结束时,从eax寄存器中获得当前进程的的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。这样,用户态进程就可以在eax寄存器中找到系统调用的返回码。
        经过的调用链为fork()--->int$0×80软中断--->ENTRY(system_call)--->ENTRY(sys_call_table)--->sys_fork()--->do_fork()。实际上fork、vfork和clone三个系统调最终都是调用do_fork()。只不过在调用时所传递的参数有所不同,而参数的不同正好导致了子进程与父进程之间对资源的共享程度不同。因此,分析do_fork()成为我们的首要任务。在进入do_fork函数进行分析之前,很有必要了解一下它的参数。
        clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过 clone标志可以有选择的对父进程的资源进行复制。本文所涉及到的clone标志详见下表。


                                                                                                表1 clone标志含义

        stack_start:子进程用户态堆栈的地址。
        regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中。
        stack_size:未被使用,通常被赋值为0。
        parent_tidptr:父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义。
        child_tidptr:子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。
        do_fork函数在./linux/kernel/fork.c中,主要工作就是复制原来的进程成为另一个新的进程,它完成了整个进程创建中的大部分工作。代码如下:

    long do_fork(unsigned long clone_flags,
    	      unsigned long stack_start,
    	      struct pt_regs *regs,
    	      unsigned long stack_size,
    	      int __user *parent_tidptr,
    	      int __user *child_tidptr)
    {
    	struct task_struct *p;
    	int trace = 0;
    	long nr;
    
    	/*
    	 * 做一些预先的参数和权限检查
    	 */
    	if (clone_flags & CLONE_NEWUSER) {
    		if (clone_flags & CLONE_THREAD)
    			return -EINVAL;
    		/* 希望当用户名称被支持时,这里的检查可去掉
    		 */
    		if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
    				!capable(CAP_SETGID))
    			return -EPERM;
    	}
    
    	/*
    	 * 希望在2.6.26之后这些标志能实现循环
    	 */
    	if (unlikely(clone_flags & CLONE_STOPPED)) {
    		static int __read_mostly count = 100;
    
    		if (count > 0 && printk_ratelimit()) {
    			char comm[TASK_COMM_LEN];
    
    			count--;
    			printk(KERN_INFO "fork(): process `%s' used deprecated "
    					"clone flags 0x%lx\n",
    				get_task_comm(comm, current),
    				clone_flags & CLONE_STOPPED);
    		}
    	}
    
    	/*
    	 * 当从kernel_thread调用本do_fork时,不使用跟踪
    	 */
    	if (likely(user_mode(regs)))	/* 如果从用户态进入本调用,则使用跟踪 */
    		trace = tracehook_prepare_clone(clone_flags);
    
    	p = copy_process(clone_flags, stack_start, regs, stack_size,
    			 child_tidptr, NULL, trace);
    	/*
    	 * 在唤醒新线程之前做下面的工作,因为新线程唤醒后本线程指针会变成无效(如果退出很快的话)
    	 */
    	if (!IS_ERR(p)) {
    		struct completion vfork;
    
    		trace_sched_process_fork(current, p);
    
    		nr = task_pid_vnr(p);
    
    		if (clone_flags & CLONE_PARENT_SETTID)
    			put_user(nr, parent_tidptr);
    
    		if (clone_flags & CLONE_VFORK) {
    			p->vfork_done = &vfork;
    			init_completion(&vfork);
    		}
    
    		audit_finish_fork(p);
    		tracehook_report_clone(regs, clone_flags, nr, p);
    
    		/*
    		 * 我们在创建时设置PF_STARTING,以防止跟踪进程想使用这个标志来区分一个完全活着的进程
    		 * 和一个还没有获得trackhook_report_clone()的进程。现在我们清除它并且设置子进程运行
    		 */
    		p->flags &= ~PF_STARTING;
    
    		if (unlikely(clone_flags & CLONE_STOPPED)) {
    			/*
    			 * 我们将立刻启动一个即时的SIGSTOP
    			 */
    			sigaddset(&p->pending.signal, SIGSTOP);
    			set_tsk_thread_flag(p, TIF_SIGPENDING);
    			__set_task_state(p, TASK_STOPPED);
    		} else {
    			wake_up_new_task(p, clone_flags);
    		}
    
    		tracehook_report_clone_complete(trace, regs,
    						clone_flags, nr, p);
    
    		if (clone_flags & CLONE_VFORK) {
    			freezer_do_not_count();
    			wait_for_completion(&vfork);
    			freezer_count();
    			tracehook_report_vfork_done(p, nr);
    		}
    	} else {
    		nr = PTR_ERR(p);
    	}
    	return nr;
    }

        (1)在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。trace表示跟踪状态,nr表示新进程的pid。接着做一些预先的参数和权限检查。
        (2)接下来检查clone_flags是否设置了CLONE_STOPPED标志。如果设置了,则做相应处理,打印消息说明进程已过时。通常这样的情况很少发生,因此在判断时使用了unlikely修饰符。使用该修饰符的判断语句执行结果与普通判断语句相同,只不过在执行效率上有所不同。正如该单词的含义所表示的那样,当前进程很少为停止状态。因此,编译器尽量不会把if内的语句与当前语句之前的代码编译在一起,以增加cache的命中率。与此相反,likely修饰符则表示所修饰的代码很可能发生。tracehook_prepare_clone用于设置子进程是否被跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。进程的ptrace字段非0说明debugger程序正在跟踪它。如果调用是从用户态进来的(而不从kernel_thread进来的),且当前进程(父进程)被另外一个进程所跟踪,那么子进程也要设置为被跟踪,并且将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。如果父进程不被跟踪,则子进程也不会被跟踪,设置好后返回trace。
        (3)接下来的这条语句要做的是整个创建过程中最核心的工作:通过copy_process()创建子进程的描述符,分配pid,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符p。该函数中的参数意义与do_fork函数相同。注意原来内核中为子进程分配pid的工作是在do_fork中完成,现在新的内核已经移到copy_process中了。
        (4)如果copy_process函数执行成功,那么将继续执行if(!IS_ERR(p))部分。首先定义了一个完成量vfork,用task_pid_vnr(p)从p中获取新进程的pid。如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行,否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用。当子进程调用exec函数或退出时就向父进程发出信号,此时父进程才会被唤醒,否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。
        (5)如果子进程被跟踪或者设置了CLONE_STOPPED标志,那么通过sigaddset函数为子进程增加挂起信号,并将子进程的状态设置为TASK_STOPPED。signal对应一个unsignedlong类型的变量,该变量的每个位分别对应一种信号。具体的操作是将SIGSTOP信号所对应的那一位置1。如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task将进程放到运行队列上,从而让调度器进行调度运行。wake_up_new_task()在./linux/kernel/sched.c中,用于唤醒第一次新创建的进程,它将为新进程做一些初始的必须的调度器统计操作,然后把进程放到运行队列中。一旦当然正在运行的进程时间片用完(通过时钟tick中断来控制),就会调用schedule(),从而进行进程调度(下一章节文章中进行详细讲解)。代码如下:

    void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
    {
    	unsigned long flags;
    	struct rq *rq;
    	int cpu = get_cpu();
    
    #ifdef CONFIG_SMP
    	rq = task_rq_lock(p, &flags);
    	p->state = TASK_WAKING;
    
    	/*
    	 * Fork balancing, do it here and not earlier because:
    	 *  - cpus_allowed can change in the fork path
    	 *  - any previously selected cpu might disappear through hotplug
    	 *
    	 * We set TASK_WAKING so that select_task_rq() can drop rq->lock
    	 * without people poking at ->cpus_allowed.
    	 */
    	cpu = select_task_rq(rq, p, SD_BALANCE_FORK, 0);
    	set_task_cpu(p, cpu);
    
    	p->state = TASK_RUNNING;
    	task_rq_unlock(rq, &flags);
    #endif
    
    	rq = task_rq_lock(p, &flags);
    	update_rq_clock(rq);
    	activate_task(rq, p, 0);
    	trace_sched_wakeup_new(rq, p, 1);
    	check_preempt_curr(rq, p, WF_FORK);
    #ifdef CONFIG_SMP
    	if (p->sched_class->task_woken)
    		p->sched_class->task_woken(rq, p);
    #endif
    	task_rq_unlock(rq, &flags);
    	put_cpu();
    }

          这里先用get_cpu()获取CPU,如果是对称多处理系统(SMP),先设置我为TASK_WAKING状态,由于有多个CPU(每个CPU上都有一个运行队列),需要进行负载均衡,选择一个最佳CPU并设置我使用这个CPU,然后设置我为TASK_RUNNING状态。这段操作是互斥的,因此需要加锁。注意TASK_RUNNING并不表示进程一定正在运行,无论进程是否正在占用CPU,只要具备运行条件,都处于该状态。 Linux把处于该状态的所有PCB组织成一个可运行队列run_queue,调度程序从这个队列中选择进程运行。事实上,Linux是将就绪态和运行态合并为了一种状态。
          然后用./linux/kernel/sched.c:activate_task()把当前进程插入到对应CPU的runqueue上,最终完成入队的函数是active_task()--->enqueue_task(),其中核心代码行为:
    p->sched_class->enqueue_task(rq, p,wakeup, head);
         sched_class在./linux/include/linux/sched.h中,是调度器一系列操作的面向对象抽象,这个类包括进程入队、出队、进程运行、进程切换等接口,用于完成对进程的调度运行。
        (6)tracehook_report_clone_complete函数用于在进程复制快要完成时报告跟踪情况。如果父进程被跟踪,则将子进程的pid赋值给父进程的进程描述符的pstrace_message字段,并向父进程的父进程发送SIGCHLD信号。
        (7)如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。
        (8)如果copy_process()在执行的时候发生错误,则先释放已分配的pid,再根据PTR_ERR()的返回值得到错误代码,保存于nr中。

     

     4. copy_process: 进程描述符的处理
         copy_process函数也在./linux/kernel/fork.c中。它会用当前进程的一个副本来创建新进程并分配pid,但不会实际启动这个新进程。它会复制寄存器中的值、所有与进程环境相关的部分,每个clone标志。新进程的实际启动由调用者来完成。

          对于每一个进程而言,内核为其单独分配了一个内存区域,这个区域存储的是内核栈和该进程所对应的一个小型进程描述符thread_info结构。在./linux/arch/x86/include/asm/thread_info.h中,如下:

    struct thread_info {
    	struct task_struct	*task;		/* 主进程描述符 */
    	struct exec_domain	*exec_domain;	/* 执行域 */
    	__u32			flags;		/* 低级别标志 */
    	__u32			status;		/* 线程同步标志 */
    	__u32			cpu;		/* 当前CPU */
    	int			preempt_count;	/* 0 => 可抢占, <0 => BUG */
    	mm_segment_t		addr_limit;
    	struct restart_block    restart_block;
    	void __user		*sysenter_return;
    #ifdef CONFIG_X86_32
    	unsigned long           previous_esp;   /* 先前栈的ESP,以防嵌入的(IRQ)栈 */
    	__u8			supervisor_stack[0];
    #endif
    	int			uaccess_err;
    };
    
    /* ... */
    
    /* 怎样从C获取当前栈指针 */
    register unsigned long current_stack_pointer asm("esp") __used;
    
    /* 怎样从C获取当前线程信息结构 */
    static inline struct thread_info *current_thread_info(void)
    {
    	return (struct thread_info *)
    		(current_stack_pointer & ~(THREAD_SIZE - 1));
    }

          之所以将线程信息结构称之为小型的进程描述符,是因为在这个结构中并没有直接包含与进程相关的字段,而是通过task字段指向具体某个进程描述符。通常这块内存区域的大小是8KB,也就是两个页的大小(有时候也使用一个页来存储,即4KB)。一个进程的内核栈和thread_info结构之间的逻辑关系如下图所示:


                                                                    图 进程内核栈和thread_info结构的存储

        从上图可知,内核栈是从该内存区域的顶层向下(从高地址到低地址)增长的,而thread_info结构则是从该区域的开始处向上(从低地址到高地址)增长。内核栈的栈顶地址存储在esp寄存器中。所以,当进程从用户态切换到内核态后,esp寄存器指向这个区域的末端。从代码的角度来看,内核栈和thread_info结构是被定义在./linux/include/linux/sched.h中的一个联合体当中的:

    union thread_union {
    	struct thread_info thread_info;
    	unsigned long stack[THREAD_SIZE/sizeof(long)];
    };

         其中,THREAD_SIZE的值取8192时,stack数组的大小为2048;THREAD_SIZE的值取4096时,stack数组的大小为1024。

    现在我们应该思考,为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)紧密的放在一起?最主要的原因就是内核可以很容易的通过esp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址。在上面的current_thread_info函数中,定义current_stack_pointer的这条内联汇编语句会从esp寄存器中获取内核栈顶地址,和~(THREAD_SIZE - 1)做与操作将屏蔽掉低13位(或12位,当THREAD_SIZE为4096时),此时所指的地址就是这片内存区域的起始地址,也就刚好是thread_info结构的地址。但是,thread_info结构的地址并不会对我们直接有用。我们通常可以轻松的通过 current宏获得当前进程的task_struct结构,前面已经列出过get_current()函数的代码。current宏返回的是thread_info结构task字段,而task正好指向与thread_info结构关联的那个进程描述符。得到 current后,我们就可以获得当前正在运行进程的描述符中任何一个字段了,比如我们通常所做的current->pid。
        下面看copy_process的实现。

    static struct task_struct *copy_process(unsigned long clone_flags,
    					unsigned long stack_start,
    					struct pt_regs *regs,
    					unsigned long stack_size,
    					int __user *child_tidptr,
    					struct pid *pid,
    					int trace)
    {
    	int retval;
    	struct task_struct *p;
    	int cgroup_callbacks_done = 0;
    
    	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
    		return ERR_PTR(-EINVAL);
    
    	/*
    	 * Thread groups must share signals as well, and detached threads
    	 * can only be started up within the thread group.
    	 */
    	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
    		return ERR_PTR(-EINVAL);
    
    	/*
    	 * Shared signal handlers imply shared VM. By way of the above,
    	 * thread groups also imply shared VM. Blocking this case allows
    	 * for various simplifications in other code.
    	 */
    	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
    		return ERR_PTR(-EINVAL);
    
    	/*
    	 * Siblings of global init remain as zombies on exit since they are
    	 * not reaped by their parent (swapper). To solve this and to avoid
    	 * multi-rooted process trees, prevent global and container-inits
    	 * from creating siblings.
    	 */
    	if ((clone_flags & CLONE_PARENT) &&
    				current->signal->flags & SIGNAL_UNKILLABLE)
    		return ERR_PTR(-EINVAL);
    
    	retval = security_task_create(clone_flags);
    	if (retval)
    		goto fork_out;
    
    	retval = -ENOMEM;
    	p = dup_task_struct(current);
    	if (!p)
    		goto fork_out;
    
    	ftrace_graph_init_task(p);
    
    	rt_mutex_init_task(p);
    
    #ifdef CONFIG_PROVE_LOCKING
    	DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
    	DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
    #endif
    	retval = -EAGAIN;
    	if (atomic_read(&p->real_cred->user->processes) >=
    			p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
    		if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
    		    p->real_cred->user != INIT_USER)
    			goto bad_fork_free;
    	}
    
    	retval = copy_creds(p, clone_flags);
    	if (retval < 0)
    		goto bad_fork_free;
    
    	/*
    	 * If multiple threads are within copy_process(), then this check
    	 * triggers too late. This doesn't hurt, the check is only there
    	 * to stop root fork bombs.
    	 */
    	retval = -EAGAIN;
    	if (nr_threads >= max_threads)
    		goto bad_fork_cleanup_count;
    
    	if (!try_module_get(task_thread_info(p)->exec_domain->module))
    		goto bad_fork_cleanup_count;
    
    	p->did_exec = 0;
    	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
    	copy_flags(clone_flags, p);
    	INIT_LIST_HEAD(&p->children);
    	INIT_LIST_HEAD(&p->sibling);
    	rcu_copy_process(p);
    	p->vfork_done = NULL;
    	spin_lock_init(&p->alloc_lock);
    
    	init_sigpending(&p->pending);
    
    	p->utime = cputime_zero;
    	p->stime = cputime_zero;
    	p->gtime = cputime_zero;
    	p->utimescaled = cputime_zero;
    	p->stimescaled = cputime_zero;
    	p->prev_utime = cputime_zero;
    	p->prev_stime = cputime_zero;
    
    	p->default_timer_slack_ns = current->timer_slack_ns;
    
    	task_io_accounting_init(&p->ioac);
    	acct_clear_integrals(p);
    
    	posix_cpu_timers_init(p);
    
    	p->lock_depth = -1;		/* -1 = no lock */
    	do_posix_clock_monotonic_gettime(&p->start_time);
    	p->real_start_time = p->start_time;
    	monotonic_to_bootbased(&p->real_start_time);
    	p->io_context = NULL;
    	p->audit_context = NULL;
    	cgroup_fork(p);
    #ifdef CONFIG_NUMA
    	p->mempolicy = mpol_dup(p->mempolicy);
     	if (IS_ERR(p->mempolicy)) {
     		retval = PTR_ERR(p->mempolicy);
     		p->mempolicy = NULL;
     		goto bad_fork_cleanup_cgroup;
     	}
    	mpol_fix_fork_child_flag(p);
    #endif
    #ifdef CONFIG_TRACE_IRQFLAGS
    	p->irq_events = 0;
    #ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
    	p->hardirqs_enabled = 1;
    #else
    	p->hardirqs_enabled = 0;
    #endif
    	p->hardirq_enable_ip = 0;
    	p->hardirq_enable_event = 0;
    	p->hardirq_disable_ip = _THIS_IP_;
    	p->hardirq_disable_event = 0;
    	p->softirqs_enabled = 1;
    	p->softirq_enable_ip = _THIS_IP_;
    	p->softirq_enable_event = 0;
    	p->softirq_disable_ip = 0;
    	p->softirq_disable_event = 0;
    	p->hardirq_context = 0;
    	p->softirq_context = 0;
    #endif
    #ifdef CONFIG_LOCKDEP
    	p->lockdep_depth = 0; /* no locks held yet */
    	p->curr_chain_key = 0;
    	p->lockdep_recursion = 0;
    #endif
    
    #ifdef CONFIG_DEBUG_MUTEXES
    	p->blocked_on = NULL; /* not blocked yet */
    #endif
    
    	p->bts = NULL;
    
    	/* Perform scheduler related setup. Assign this task to a CPU. */
    	sched_fork(p, clone_flags);
    
    	retval = perf_event_init_task(p);
    	if (retval)
    		goto bad_fork_cleanup_policy;
    
    	if ((retval = audit_alloc(p)))
    		goto bad_fork_cleanup_policy;
    	/* copy all the process information */
    	if ((retval = copy_semundo(clone_flags, p)))
    		goto bad_fork_cleanup_audit;
    	if ((retval = copy_files(clone_flags, p)))
    		goto bad_fork_cleanup_semundo;
    	if ((retval = copy_fs(clone_flags, p)))
    		goto bad_fork_cleanup_files;
    	if ((retval = copy_sighand(clone_flags, p)))
    		goto bad_fork_cleanup_fs;
    	if ((retval = copy_signal(clone_flags, p)))
    		goto bad_fork_cleanup_sighand;
    	if ((retval = copy_mm(clone_flags, p)))
    		goto bad_fork_cleanup_signal;
    	if ((retval = copy_namespaces(clone_flags, p)))
    		goto bad_fork_cleanup_mm;
    	if ((retval = copy_io(clone_flags, p)))
    		goto bad_fork_cleanup_namespaces;
    	retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
    	if (retval)
    		goto bad_fork_cleanup_io;
    
    	if (pid != &init_struct_pid) {
    		retval = -ENOMEM;
    		pid = alloc_pid(p->nsproxy->pid_ns);
    		if (!pid)
    			goto bad_fork_cleanup_io;
    
    		if (clone_flags & CLONE_NEWPID) {
    			retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
    			if (retval < 0)
    				goto bad_fork_free_pid;
    		}
    	}
    
    	p->pid = pid_nr(pid);
    	p->tgid = p->pid;
    	if (clone_flags & CLONE_THREAD)
    		p->tgid = current->tgid;
    
    	if (current->nsproxy != p->nsproxy) {
    		retval = ns_cgroup_clone(p, pid);
    		if (retval)
    			goto bad_fork_free_pid;
    	}
    
    	p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
    	/*
    	 * Clear TID on mm_release()?
    	 */
    	p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
    #ifdef CONFIG_FUTEX
    	p->robust_list = NULL;
    #ifdef CONFIG_COMPAT
    	p->compat_robust_list = NULL;
    #endif
    	INIT_LIST_HEAD(&p->pi_state_list);
    	p->pi_state_cache = NULL;
    #endif
    	/*
    	 * sigaltstack should be cleared when sharing the same VM
    	 */
    	if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
    		p->sas_ss_sp = p->sas_ss_size = 0;
    
    	/*
    	 * Syscall tracing should be turned off in the child regardless
    	 * of CLONE_PTRACE.
    	 */
    	clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
    #ifdef TIF_SYSCALL_EMU
    	clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
    #endif
    	clear_all_latency_tracing(p);
    
    	/* ok, now we should be set up.. */
    	p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
    	p->pdeath_signal = 0;
    	p->exit_state = 0;
    
    	/*
    	 * Ok, make it visible to the rest of the system.
    	 * We dont wake it up yet.
    	 */
    	p->group_leader = p;
    	INIT_LIST_HEAD(&p->thread_group);
    
    	/* Now that the task is set up, run cgroup callbacks if
    	 * necessary. We need to run them before the task is visible
    	 * on the tasklist. */
    	cgroup_fork_callbacks(p);
    	cgroup_callbacks_done = 1;
    
    	/* Need tasklist lock for parent etc handling! */
    	write_lock_irq(&tasklist_lock);
    
    	/* CLONE_PARENT re-uses the old parent */
    	if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
    		p->real_parent = current->real_parent;
    		p->parent_exec_id = current->parent_exec_id;
    	} else {
    		p->real_parent = current;
    		p->parent_exec_id = current->self_exec_id;
    	}
    
    	spin_lock(¤t->sighand->siglock);
    
    	/*
    	 * Process group and session signals need to be delivered to just the
    	 * parent before the fork or both the parent and the child after the
    	 * fork. Restart if a signal comes in before we add the new process to
    	 * it's process group.
    	 * A fatal signal pending means that current will exit, so the new
    	 * thread can't slip out of an OOM kill (or normal SIGKILL).
     	 */
    	recalc_sigpending();
    	if (signal_pending(current)) {
    		spin_unlock(¤t->sighand->siglock);
    		write_unlock_irq(&tasklist_lock);
    		retval = -ERESTARTNOINTR;
    		goto bad_fork_free_pid;
    	}
    
    	if (clone_flags & CLONE_THREAD) {
    		atomic_inc(¤t->signal->count);
    		atomic_inc(¤t->signal->live);
    		p->group_leader = current->group_leader;
    		list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
    	}
    
    	if (likely(p->pid)) {
    		list_add_tail(&p->sibling, &p->real_parent->children);
    		tracehook_finish_clone(p, clone_flags, trace);
    
    		if (thread_group_leader(p)) {
    			if (clone_flags & CLONE_NEWPID)
    				p->nsproxy->pid_ns->child_reaper = p;
    
    			p->signal->leader_pid = pid;
    			tty_kref_put(p->signal->tty);
    			p->signal->tty = tty_kref_get(current->signal->tty);
    			attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
    			attach_pid(p, PIDTYPE_SID, task_session(current));
    			list_add_tail_rcu(&p->tasks, &init_task.tasks);
    			__get_cpu_var(process_counts)++;
    		}
    		attach_pid(p, PIDTYPE_PID, pid);
    		nr_threads++;
    	}
    
    	total_forks++;
    	spin_unlock(¤t->sighand->siglock);
    	write_unlock_irq(&tasklist_lock);
    	proc_fork_connector(p);
    	cgroup_post_fork(p);
    	perf_event_fork(p);
    	return p;
    
    bad_fork_free_pid:
    	if (pid != &init_struct_pid)
    		free_pid(pid);
    bad_fork_cleanup_io:
    	put_io_context(p->io_context);
    bad_fork_cleanup_namespaces:
    	exit_task_namespaces(p);
    bad_fork_cleanup_mm:
    	if (p->mm)
    		mmput(p->mm);
    bad_fork_cleanup_signal:
    	if (!(clone_flags & CLONE_THREAD))
    		__cleanup_signal(p->signal);
    bad_fork_cleanup_sighand:
    	__cleanup_sighand(p->sighand);
    bad_fork_cleanup_fs:
    	exit_fs(p); /* blocking */
    bad_fork_cleanup_files:
    	exit_files(p); /* blocking */
    bad_fork_cleanup_semundo:
    	exit_sem(p);
    bad_fork_cleanup_audit:
    	audit_free(p);
    bad_fork_cleanup_policy:
    	perf_event_free_task(p);
    #ifdef CONFIG_NUMA
    	mpol_put(p->mempolicy);
    bad_fork_cleanup_cgroup:
    #endif
    	cgroup_exit(p, cgroup_callbacks_done);
    	delayacct_tsk_free(p);
    	module_put(task_thread_info(p)->exec_domain->module);
    bad_fork_cleanup_count:
    	atomic_dec(&p->cred->user->processes);
    	exit_creds(p);
    bad_fork_free:
    	free_task(p);
    fork_out:
    	return ERR_PTR(retval);
    }

        (1)定义返回值亦是retval和新的进程描述符task_struct结构p。
        (2)标志合法性检查。对clone_flags所传递的标志组合进行合法性检查。当出现以下三种情况时,返回出错代号:
        1)CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。在传统的Unix系统中,整个系统只有一个已经安装的文件系统树。每个进程从系统的根文件系统开始,通过合法的路径可以访问任何文件。在2.6版本中的内核中,每个进程都可以拥有属于自己的已安装文件系统树,也被称为命名空间。通常大多数进程都共享init进程所使用的已安装文件系统树,只有在clone_flags中设置了CLONE_NEWNS标志时,才会为此新进程开辟一个新的命名空间。
        2)CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
        3)CLONE_SIGHAND被设置,但CLONE_VM未被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。
        (3)安全性检查。通过调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。询问 Linux Security Module (LSM) 看当前任务是否可以创建一个新任务。LSM是SELinux的核心。
        (4)复制进程描述符。通过dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。注意,这里将当前进程描述符指针作为参数传递到此函数中。函数代码如下:

    int __attribute__((weak)) arch_dup_task_struct(struct task_struct *dst,
    					       struct task_struct *src)
    {
    	*dst = *src;
    	return 0;
    }
    
    static struct task_struct *dup_task_struct(struct task_struct *orig)
    {
    	struct task_struct *tsk;
    	struct thread_info *ti;
    	unsigned long *stackend;
    
    	int err;
    
    	prepare_to_copy(orig);
    
    	tsk = alloc_task_struct();
    	if (!tsk)
    		return NULL;
    
    	ti = alloc_thread_info(tsk);
    	if (!ti) {
    		free_task_struct(tsk);
    		return NULL;
    	}
    
     	err = arch_dup_task_struct(tsk, orig);
    	if (err)
    		goto out;
    
    	tsk->stack = ti;
    
    	err = prop_local_init_single(&tsk->dirties);
    	if (err)
    		goto out;
    
    	setup_thread_stack(tsk, orig);
    	stackend = end_of_stack(tsk);
    	*stackend = STACK_END_MAGIC;	/* 用于溢出检测 */
    
    #ifdef CONFIG_CC_STACKPROTECTOR
    	tsk->stack_canary = get_random_int();
    #endif
    
    	/* One for us, one for whoever does the "release_task()" (usually parent) */
    	atomic_set(&tsk->usage,2);
    	atomic_set(&tsk->fs_excl, 0);
    #ifdef CONFIG_BLK_DEV_IO_TRACE
    	tsk->btrace_seq = 0;
    #endif
    	tsk->splice_pipe = NULL;
    
    	account_kernel_stack(ti, 1);
    
    	return tsk;
    
    out:
    	free_thread_info(ti);
    	free_task_struct(tsk);
    	return NULL;
    }

         首先,该函数分别定义了指向task_struct和thread_info结构体的指针。接着,prepare_to_copy为正式的分配进程描述符做一些准备工作。主要是将一些必要的寄存器的值保存到父进程的thread_info结构中。这些值会在稍后被复制到子进程的thread_info结构中。执行alloc_task_struct宏,该宏负责为子进程的进程描述符分配空间,将该片内存的首地址赋值给tsk,随后检查这片内存是否分配正确。执行alloc_thread_info宏,为子进程获取一块空闲的内存区,用来存放子进程的内核栈和thread_info结构,并将此会内存区的首地址赋值给ti变量,随后检查是否分配正确。
         上面已经说明过orig是传进来的current宏,指向当前进程描述符的指针。arch_dup_task_struct直接将orig指向的当前进程描述符内容复制到当前里程描述符tsk。接着,用atomic_set将子进程描述符的使用计数器设置为2,表示该进程描述符正在被使用并且处于活动状态。最后返回指向刚刚创建的子进程描述符内存区的指针。
         通过dup_task_struct可以看到,当这个函数成功操作之后,子进程和父进程的描述符中的内容是完全相同的。在稍后的copy_process代码中,我们将会看到子进程逐步与父进程区分开来。
        (5)一些初始化。通过诸如ftrace_graph_init_task,rt_mutex_init_task完成某些数据结构的初始化。调用copy_creds()复制证书(应该是复制权限及身份信息)。
        (6)检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。
        (7)复制标志。通过copy_flags,将从do_fork()传递来的的clone_flags和pid分别赋值给子进程描述符中的对应字段。
        (8)初始化子进程描述符。初始化其中的各个字段,使得子进程和父进程逐渐区别出来。这部分工作包含初始化子进程中的children和sibling等队列头、初始化自旋锁和信号处理、初始化进程统计信息、初始化POSIX时钟、初始化调度相关的统计信息、初始化审计信息。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不足为奇了。
        (9)调度器设置。调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。
        (10)复制进程的所有信息。根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构。比如copy_semundo()、复制开放文件描述符(copy_files)、复制符号信息(copy_sighand 和copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。
        (11)复制线程。通过copy_threads()函数更新子进程的内核栈和寄存器中的值。在之前的dup_task_struct()中只是为子进程创建一个内核栈,至此才是真正的赋予它有意义的值。
        当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,这也就是为什么使用fork()时子进程返回值是0。而在do_fork函数中则返回的是子进程的pid,这一点在上述内容中我们已经有所分析。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址。
        (12)分配pid。用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。成功则赋给p->pid。
        (13)更新属性和进程数量。根据clone_flags的值继续更新子进程的某些属性。将 nr_threads加一,表明新进程已经被加入到进程集合中。将total_forks加一,以记录被创建进程数量。
        (14)如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。
        至此,copy_process()的大致执行过程分析完毕。
        copy_process()执行完后返回do_fork(),do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。至于子进程何时执行这完全取决于调度程序,也就是schedule()的事了。


        5. 进程调度
           创建好的进程最后被插入到运行队列中,它会通过 Linux 调度程序来调度。Linux调度程序维护了针对每个优先级别的一组列表,其中保存了 task_struct 引用。当正在运行的进程时间片用完时,时钟tick产生中断,调用kernel/sched.c:scheduler_tick()进程调度器的中断处理,中断返回后就会调用schedule()。运行队列中的任务通过 schedule 函数(在./linux/kernel/sched.c 内)来调用,它根据加载及进程执行历史决定最佳进程。这里并不涉及此函数的分析。


        6. 进程销毁
         进程销毁可以通过几个事件驱动、通过正常的进程结束(当一个C程序从main函数返回时startuproutine调用exit)、通过信号或是通过显式地对 exit 函数的调用。不管进程如何退出,进程的结束都要借助对内核函数 do_exit(在./linux/kernel/exit.c 内)的调用。函数的层次结构如下图:

                                               图 进程销毁的函数层次结构

        exit()调用通过0x80中断跳到sys_exit内核例程处,这个例程名称可以在./linux/include/linux/syscalls.h中找到(syscalls.h中导出所有平台无关的系统调用名称),定义为asmlinkage long sys_exit(int error_code); 它会直接调用do_exit。代码如下:

    NORET_TYPE void do_exit(long code)
    {
    	struct task_struct *tsk = current;
    	int group_dead;
    
    	profile_task_exit(tsk);
    
    	WARN_ON(atomic_read(&tsk->fs_excl));
    
    	if (unlikely(in_interrupt()))
    		panic("Aiee, killing interrupt handler!");
    	if (unlikely(!tsk->pid))
    		panic("Attempted to kill the idle task!");
    
    	/*
    	 * If do_exit is called because this processes oopsed, it's possible
    	 * that get_fs() was left as KERNEL_DS, so reset it to USER_DS before
    	 * continuing. Amongst other possible reasons, this is to prevent
    	 * mm_release()->clear_child_tid() from writing to a user-controlled
    	 * kernel address.
    	 */
    	set_fs(USER_DS);
    
    	tracehook_report_exit(&code);
    
    	validate_creds_for_do_exit(tsk);
    
    	/*
    	 * We're taking recursive faults here in do_exit. Safest is to just
    	 * leave this task alone and wait for reboot.
    	 */
    	if (unlikely(tsk->flags & PF_EXITING)) {
    		printk(KERN_ALERT
    			"Fixing recursive fault but reboot is needed!\n");
    		/*
    		 * We can do this unlocked here. The futex code uses
    		 * this flag just to verify whether the pi state
    		 * cleanup has been done or not. In the worst case it
    		 * loops once more. We pretend that the cleanup was
    		 * done as there is no way to return. Either the
    		 * OWNER_DIED bit is set by now or we push the blocked
    		 * task into the wait for ever nirwana as well.
    		 */
    		tsk->flags |= PF_EXITPIDONE;
    		set_current_state(TASK_UNINTERRUPTIBLE);
    		schedule();
    	}
    
    	exit_irq_thread();
    
    	exit_signals(tsk);  /* sets PF_EXITING */
    	/*
    	 * tsk->flags are checked in the futex code to protect against
    	 * an exiting task cleaning up the robust pi futexes.
    	 */
    	smp_mb();
    	spin_unlock_wait(&tsk->pi_lock);
    
    	if (unlikely(in_atomic()))
    		printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
    				current->comm, task_pid_nr(current),
    				preempt_count());
    
    	acct_update_integrals(tsk);
    
    	group_dead = atomic_dec_and_test(&tsk->signal->live);
    	if (group_dead) {
    		hrtimer_cancel(&tsk->signal->real_timer);
    		exit_itimers(tsk->signal);
    		if (tsk->mm)
    			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
    	}
    	acct_collect(code, group_dead);
    	if (group_dead)
    		tty_audit_exit();
    	if (unlikely(tsk->audit_context))
    		audit_free(tsk);
    
    	tsk->exit_code = code;
    	taskstats_exit(tsk, group_dead);
    
    	exit_mm(tsk);
    
    	if (group_dead)
    		acct_process();
    	trace_sched_process_exit(tsk);
    
    	exit_sem(tsk);
    	exit_files(tsk);
    	exit_fs(tsk);
    	check_stack_usage();
    	exit_thread();
    	cgroup_exit(tsk, 1);
    
    	if (group_dead && tsk->signal->leader)
    		disassociate_ctty(1);
    
    	module_put(task_thread_info(tsk)->exec_domain->module);
    
    	proc_exit_connector(tsk);
    
    	/*
    	 * Flush inherited counters to the parent - before the parent
    	 * gets woken up by child-exit notifications.
    	 */
    	perf_event_exit_task(tsk);
    
    	exit_notify(tsk, group_dead);
    #ifdef CONFIG_NUMA
    	mpol_put(tsk->mempolicy);
    	tsk->mempolicy = NULL;
    #endif
    #ifdef CONFIG_FUTEX
    	if (unlikely(current->pi_state_cache))
    		kfree(current->pi_state_cache);
    #endif
    	/*
    	 * Make sure we are holding no locks:
    	 */
    	debug_check_no_locks_held(tsk);
    	/*
    	 * We can do this unlocked here. The futex code uses this flag
    	 * just to verify whether the pi state cleanup has been done
    	 * or not. In the worst case it loops once more.
    	 */
    	tsk->flags |= PF_EXITPIDONE;
    
    	if (tsk->io_context)
    		exit_io_context();
    
    	if (tsk->splice_pipe)
    		__free_pipe_info(tsk->splice_pipe);
    
    	validate_creds_for_do_exit(tsk);
    
    	preempt_disable();
    	exit_rcu();
    	/* causes final put_task_struct in finish_task_switch(). */
    	tsk->state = TASK_DEAD;
    	schedule();
    	BUG();
    	/* Avoid "noreturn function does return".  */
    	for (;;)
    		cpu_relax();	/* For when BUG is null */
    }
    
    EXPORT_SYMBOL_GPL(do_exit);

        (1)为进程销毁做一系列准备。用set_fs设置USER_DS。注意如果do_exit是因为当前进程出现不可预知的错误而被调用,这时get_fs()有可能得到的仍然是KERNEL_DS状态,因此我们要重置它为USER_DS状态。还有一个可能原因是这可以防止mm_release()->clear_child_tid()写一个被用户控制的内核地址。
        (2)清除所有信号处理函数。exit_signals函数会设置PF_EXITING标志来表明进程正在退出,并清除所有信息处理函数。内核的其他方面会利用PF_EXITING来防止在进程被删除时还试图处理此进程。
        (3)清除一系列的进程资源。比如比如 exit_mm删除内存页、exit_files关闭所有打开的文件描述符,这会清理I/O缓存,如果缓存中有数据,就会将它们写入相应的文件,以防止文件数据的丢失。exit_fs清除当前目录关联的inode、exit_thread清除线程信息、等等。
        (4)发出退出通知。调用exit_notify执行一系列通知。例如通知父进程我正在退出。如下:

    static void exit_notify(struct task_struct *tsk, int group_dead)
    {
    	int signal;
    	void *cookie;
    
    	/*
    	 * This does two things:
    	 *
      	 * A.  Make init inherit all the child processes
    	 * B.  Check to see if any process groups have become orphaned
    	 *	as a result of our exiting, and if they have any stopped
    	 *	jobs, send them a SIGHUP and then a SIGCONT.  (POSIX 3.2.2.2)
    	 */
    	forget_original_parent(tsk);
    	exit_task_namespaces(tsk);
    
    	write_lock_irq(&tasklist_lock);
    	if (group_dead)
    		kill_orphaned_pgrp(tsk->group_leader, NULL);
    
    	/* Let father know we died
    	 *
    	 * Thread signals are configurable, but you aren't going to use
    	 * that to send signals to arbitary processes.
    	 * That stops right now.
    	 *
    	 * If the parent exec id doesn't match the exec id we saved
    	 * when we started then we know the parent has changed security
    	 * domain.
    	 *
    	 * If our self_exec id doesn't match our parent_exec_id then
    	 * we have changed execution domain as these two values started
    	 * the same after a fork.
    	 */
    	if (tsk->exit_signal != SIGCHLD && !task_detached(tsk) &&
    	    (tsk->parent_exec_id != tsk->real_parent->self_exec_id ||
    	     tsk->self_exec_id != tsk->parent_exec_id))
    		tsk->exit_signal = SIGCHLD;
    
    	signal = tracehook_notify_death(tsk, &cookie, group_dead);
    	if (signal >= 0)
    		signal = do_notify_parent(tsk, signal);
    
    	tsk->exit_state = signal == DEATH_REAP ? EXIT_DEAD : EXIT_ZOMBIE;
    
    	/* mt-exec, de_thread() is waiting for us */
    	if (thread_group_leader(tsk) &&
    	    tsk->signal->group_exit_task &&
    	    tsk->signal->notify_count < 0)
    		wake_up_process(tsk->signal->group_exit_task);
    
    	write_unlock_irq(&tasklist_lock);
    
    	tracehook_report_death(tsk, signal, cookie, group_dead);
    
    	/* If the process is dead, release it - nobody will wait for it */
    	if (signal == DEATH_REAP)
    		release_task(tsk);
    }

         exit_notify将当前进程的所有子进程的父进程ID设置为1(init),让init接管所有这些子进程。如果当前进程是某个进程组的组长,其销毁导致进程组变为“无领导状态“,则向每个组内进程发送挂起信号SIGHUP,然后发送SIGCONT。这是遵循POSIX3.2.2.2标准。接着向自己的父进程发送SIGCHLD信号,然后调用do_notify_parent通知父进程。若返回DEATH_REAP(这个意思是不管是否有其他进程关心本进程的退出信息,自动完成进程退出和PCB销毁),就直接进入EXIT_DEAD状态,如果不是,则就需要变为EXIT_ZOMBIE状态。
         注意在最初父进程创建子进程时,如果调用了waitpid()等待子进程结束(表示它关心子进程的状态),子进程结束时父进程会处理它发来的SIGCHILD信号。如果不调用wait(表示它不关心子进程的死活状态),则不会处理子进程的SIGCHILD信号。参看./linux/kernel/signal.c:do_notify_parent(),代码如下:

    int do_notify_parent(struct task_struct *tsk, int sig)
    {
    	struct siginfo info;
    	unsigned long flags;
    	struct sighand_struct *psig;
    	int ret = sig;
    
    	BUG_ON(sig == -1);
    
     	/* do_notify_parent_cldstop should have been called instead.  */
     	BUG_ON(task_is_stopped_or_traced(tsk));
    
    	BUG_ON(!task_ptrace(tsk) &&
    	       (tsk->group_leader != tsk || !thread_group_empty(tsk)));
    
    	info.si_signo = sig;
    	info.si_errno = 0;
    	/*
    	 * we are under tasklist_lock here so our parent is tied to
    	 * us and cannot exit and release its namespace.
    	 *
    	 * the only it can is to switch its nsproxy with sys_unshare,
    	 * bu uncharing pid namespaces is not allowed, so we'll always
    	 * see relevant namespace
    	 *
    	 * write_lock() currently calls preempt_disable() which is the
    	 * same as rcu_read_lock(), but according to Oleg, this is not
    	 * correct to rely on this
    	 */
    	rcu_read_lock();
    	info.si_pid = task_pid_nr_ns(tsk, tsk->parent->nsproxy->pid_ns);
    	info.si_uid = __task_cred(tsk)->uid;
    	rcu_read_unlock();
    
    	info.si_utime = cputime_to_clock_t(cputime_add(tsk->utime,
    				tsk->signal->utime));
    	info.si_stime = cputime_to_clock_t(cputime_add(tsk->stime,
    				tsk->signal->stime));
    
    	info.si_status = tsk->exit_code & 0x7f;
    	if (tsk->exit_code & 0x80)
    		info.si_code = CLD_DUMPED;
    	else if (tsk->exit_code & 0x7f)
    		info.si_code = CLD_KILLED;
    	else {
    		info.si_code = CLD_EXITED;
    		info.si_status = tsk->exit_code >> 8;
    	}
    
    	psig = tsk->parent->sighand;
    	spin_lock_irqsave(&psig->siglock, flags);
    	if (!task_ptrace(tsk) && sig == SIGCHLD &&
    	    (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
    	     (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
    		/*
    		 * We are exiting and our parent doesn't care.  POSIX.1
    		 * defines special semantics for setting SIGCHLD to SIG_IGN
    		 * or setting the SA_NOCLDWAIT flag: we should be reaped
    		 * automatically and not left for our parent's wait4 call.
    		 * Rather than having the parent do it as a magic kind of
    		 * signal handler, we just set this to tell do_exit that we
    		 * can be cleaned up without becoming a zombie.  Note that
    		 * we still call __wake_up_parent in this case, because a
    		 * blocked sys_wait4 might now return -ECHILD.
    		 *
    		 * Whether we send SIGCHLD or not for SA_NOCLDWAIT
    		 * is implementation-defined: we do (if you don't want
    		 * it, just use SIG_IGN instead).
    		 */
    		ret = tsk->exit_signal = -1;
    		if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
    			sig = -1;
    	}
    	if (valid_signal(sig) && sig > 0)
    		__group_send_sig_info(sig, &info, tsk->parent);
    	__wake_up_parent(tsk, tsk->parent);
    	spin_unlock_irqrestore(&psig->siglock, flags);
    
    	return ret;
    }

    我们可以看到,如果父进程显示指定对子进程的SIGCHLD信号处理为SIG_IGN,或者标志为SA_NOCLDWAIT,则返回的是ret=-1,即DEATH_REAP(这个宏在./linux/include/tracehook.h中定义为-1),这时在exit_notify中子进程马上变为EXIT_DEAD,表示我已退出并且死亡,最后被后面的release_task回收,将不会再有进程等待我。否则返回值与传入的信号值相同,子进程变成EXIT_ZOMBIE,表示已退出但还没死。不管有没有处理SIGCHLD,do_notify_parent最后都会用__wake_up_parent来唤醒正在等待的父进程。
          可见子进程在结束前不一定都需要经过一个EXIT_ZOMBIE过程。如果父进程调用了waitpid等待子进程,则会显示处理它发来的SIGCHILD信号,子进程结束时会自我清理(在do_exit中自己用release_task清理);如果父进程没有调用waitpid等待子进程,则不会处理SIGCHLD信号,子进程不会马上被清理,而是变成EXIT_ZOMBIE状态,成为著名的僵尸进程。还有一种特殊情形,如果子进程退出时父进程恰好正在睡眠(sleep),导致没来得急处理SIGCHLD,子进程也会成为僵尸,只要父进程在醒来后能调用waitpid,也能清理僵尸子进程,因为wait系统调用内部有清理僵尸子进程的代码。因此,如果父进程一直没有调用waitpid,那么僵尸子进程就只能等到父进程退出时被init接管了。init进程会负责清理这些僵尸进程(init肯定会调用wait)。
         我们可以写个简单的程序来验证,父进程创建10个子进程,子进程sleep一段时间后退出。第一种情况,父进程只对1~9号子进程调用waitpid(),1~9号子进程都正常结束,而在父进程结束前,pids[0]为EXIT_ZOMBIE。第二种情况,父进程创建10个子进程后,sleep()一段时间,在这段时间内_exit()的子进程都成为EXIT_ZOMBIE。父进程sleep()结束后,依次调用waitpid(),子进程马上变为EXIT_DEAD被清理。
         为了更好地理解怎么清理僵尸进程,我们简要地分析一下wait系统调用。wait族的系统调用如waitpid,wait4等,最后都会进入./linux/kernel/exit.c:do_wait()内核例程,而后函数链为do_wait()--->do_wait_thread()--->wait_consider_task(),在这里,如果子进程在exit_notify中设置的tsk->exit_state为EXIT_DEAD,就返回0,即wait系统调用返回,说明子进程不是僵尸进程,会自己用release_task进行回收。如果它的exit_state是EXIT_ZOMBIE,进入wait_task_zombie()。在这里使用xchg尝试把它的exit_state设置为EXIT_DEAD,可见父进程的wait4调用会把子进程由EXIT_ZOMBIE设置为EXIT_DEAD。最后wait_task_zombie()在末尾调用release_task()清理这个僵尸进程。
        (5)设置销毁标志并调度新的进程。在do_exit的最后,用exit_io_context清除IO上下文、preempt_disable禁用抢占,设置进程状态为TASK_DEAD,然后调用./linux/kernel/sched.c:schedule()来选择一个将要执行的新进程。注意进程在退出并回收之后,其位于调度器的进程列表中的进程描述符(PCB)并没有立即释放,必须在设置task_struct的state为TASK_DEAD之后,由schedule()中的finish_task_switch()--->put_task_struct()把它的PCB重新放回到freelist(可用列表)中,这时PCB才算释放,然后切换到新的进程。

     

       7. exit与_exit的差异
        为了理解这两个系统调用的差异,先来讨论文件内存缓存区的问题。 在linux中,标准输入输出(I/O)函数都是作为文件来处理。对应于打开的每个文件,在内存中都有对应的缓存,每次读取文件时,会多读一些记录到缓存中,这样在下次读文件时,就在缓存中读取;同样,在写文件时也是写在文件对应的缓存中,并不是直接写入硬盘的文件中,等满足了一定条件(如达到一定数量,遇到换行符\n或文件结束标志EOF)才将数据真正的写入文件。这样做的好处就是加快了文件读写的速度。但这样也带来了一些问题,比如有一些数据,我们认为已经写入了文件,但实际上没有满足一定条件而任然驻留在内存的缓存中,这样,如果我们直接用_exit()函数直接终止进程,将导致数据丢失。如果改成exit,就不会有数据丢失的问题出现了,这就是它们之间的区别了.要解释这个问题,就要涉及它们的工作步骤了。
       exit():通过前面源代码分析可知,在执行该函数时,进程会检查文件打开情况,清理I/O缓存,如果缓存中有数据,就会将它们写入相应的文件,这样就防止了文件数据的丢失,然后终止进程。
       _exit():在执行该函数时,并不清理标准输入输出缓存,而是直接清除内存空间,当然也就把文件缓存中尚未写入文件的数据给销毁了。由此可见,使用exit()函数更加安全。
       此外,对于它们两者的区别还有各自的头文件不同。exit()在stdlib.h中,_exit()在unistd.h中。一般情况下exit(0)表示正常退出,exit(1),exit(-1)为异常退出,0、1、-1是返回值,具体含义可以自定。还要注意return是返回函数调用,如果返回的是main函数,则为退出程序 。exit是在调用处强行退出程序,运行一次程序就结束。

        下面是完整的Linux进程运行流程:

    arch/x86/include/asm/unistd_32.h:fork()        用户空间来调用(如C程序)
    	--->int $0×80		产生0x80软中断
    	--->arch/x86/kernel/entry_32.S:ENTRY(system_call)  中断处理程序system_call()
    		--->执行SAVE_ALL宏		保存所有CPU寄存器值
    		--->arch/x86/kernel/syscall_table_32.S:ENTRY(sys_call_table) 系统调用多路分解表
    	--->arch/x86/kernel/process_32.c:sys_fork()
    		--->kernel/fork.c:do_fork()  复制原来的进程成为另一个新的进程
    			--->kernel/fork.c:copy_process()
    				--->struct task_struct *p;  定义新的进程描述符(PCB)
    				--->clone_flags标志的合法性检查
    				--->security_task_create()    安全性检查(SELinux机制)
    				--->kernel/fork.c:dup_task_struct()   复制进程描述符
    					--->struct thread_info *ti;  定义线程信息结构
    					--->alloc_task_struct()      为新的PCB分配内存
    					--->kernel/fork.c:arch_dup_task_struct()  复制父进程的PCB
    					--->atomic_set(&tsk->usage,2)    将PCB使用计数器设置为2,表示活动状态
    				--->copy_creds()   复制权限及身份信息
    				--->检测进程总数是否超过max_threads
    				--->初始化PCB中各个字段
    				--->sched_fork()    调度器相关设置
    				--->复制进程所有信息copy_semundo(), copy_files(),
    				--->copy_signal(), copy_mm()
    				--->copy_thread()    复制线程
    				--->alloc_pid()    分配pid
    				--->更新属性和进程数量计数
    			--->kernel/sched.c:wake_up_new_task()  把进程放到运行队列上,让调度器进行调度
    				--->kernel/sched.c:select_task_rq()  选择最佳的CPU(SMP中有多个CPU)
    				--->p->state = TASK_RUNNING    设置成TASK_RUNNING状态
    				--->activate_task()
    					--->enqueue_task()  把当前进程插入到对应CPU的runqueue上
    			--->有CLONE_VFORK标志:wait_for_completion()  让父进程阻塞,等待子进程结束
    			--->返回分配的pid
    kernel/sched.c:schedule()    调度新创建的进程
    进程运行中
    exit()        用户空间来调用(如C程序)
    	--->0x80中断跳转到include/linux/syscalls.h:sys_exit()
    		--->kernel/exit.c:do_exit()    负责进程的退出
    			--->struct task_struct *tsk = current;    获取我的PCB
    			--->set_fs(USER_DS)    设置使用的文件系统模式
    			--->exit_signals()     清除信号处理函数并设置PF_EXITING标志
    			--->清除进程一系列资源exit_mm(), exit_files() 
    			--->exit_fs(), exit_thread()
    			--->kernel/exit.c:exit_notify()  退出通知
    				--->forget_original_parent()  把我的所有子进程过继给init进程
    				--->kill_orphaned_pgrp()      向进程组内各进程发送挂起信号SIGHUP及SIGCONT
    				--->tsk->exit_signal = SIGCHLD;  向我的父进程发送SIGCHLD信号
    				--->kernel/exit.c:do_notify_parent()  通知父进程
    					--->如果父进程处理SIGCHLD信号,返回DEATH_REAP
    					--->如果父进程不处理SIGCHLD信号,返回传入时的信号值
    					--->__wake_up_parent()    唤醒父进程
    				--->通知返回DEATH_REAP,设置exit_state为EXIT_DEAD    我退出并且死亡
    				--->否则设置我为EXIT_ZOMBIE      我退出但没死亡,成为僵尸进程
    				--->如果为DEATH_REAP:release_task()    我自己清理相关资源
    				--->如果为僵尸,在我的父进程退出时我会过继给init进程,由init负责清理
    			--->exit_io_context()    清理IO上下文
    			--->preempt_disable()    禁用抢占
    			--->tsk->state = TASK_DEAD;    设置我为进程死亡状态
    			--->kernel/sched.c:schedule()  释放我的PCB,调度另一个新的进程
    
    清理僵尸进程:wait系统调用			等待子进程结束
    	--->0x80中断最后到达kernel/exit.c:do_wait()
    		--->do_wait_thread()
    			--->wait_consider_task()
    				--->如果子进程为EXIT_DEAD,返回0,wait调用返回,子进程自己清理自己
    				--->如果子进程为EXIT_ZOMBIE:wait_task_zombie()
    					--->xchg()    设置僵尸子进程为EXIT_DEAD
    					--->release_task()    清理僵尸子进程

        下面是基本的执行流程图:


                                               图 Linux进程管理的执行流程

     

    展开全文
  • //init.c #include "common.h" int main(int argc, char const *argv[]) { key_t key; int semid; //semaphore id int shmid; //shared memory id /* Create key*/ key = KEY_NUM; /* Initialize ...}
  • Linux进程管理补充.ppt

    2020-08-26 21:39:39
    Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;Linux进程管理补充;进程状态和标志;进程的族亲关系;进程间...
  • Linux系列第五谈(Linux磁盘管理、Linux进程管理

    千次阅读 多人点赞 2020-08-04 19:13:46
    Linux磁盘管理、Linux进程管理

    在这里插入图片描述

    写博客即是为了记录自己的学习历程,也希望能够结交志同道合的朋友一起学习。文章在撰写过程中难免有疏漏和错误,欢迎你在下方留言指出文章的不足之处;更多内容请点进👉我的博客K👈阅览。
    临渊羡鱼,不如退而结网。一起加油!

    Linux系列第一谈:阿里云服务器基本Linux操作(开启安全组、使用面板搭建环境 使用命令行搭建环境 安装jdk、tomcat、Docker)

    Linux系列第二谈(开机关机、Linux中的文件、目录管理、基本属性)

    Linux系列第三谈(Linux常用命令:文件查看、Linux软硬链接、Vim编辑器)

    Linux系列第四谈(Linux中的账号管理)

    一、磁盘管理

    1、概述

    Linux磁盘管理好坏直接关系到整个系统的性能问题。
    Linux磁盘管理常用命令为 df、du。

    • df :列出文件系统的整体磁盘使用量
    • du:检查磁盘空间使用量

    2、df

    df命令参数功能:检查文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息。

    语法:

    df [-ahikHTm] [目录或文件名]
    

    选项与参数:

    • -a :列出所有的文件系统,包括系统特有的 /proc 等文件系统;
    • -k :以 KBytes 的容量显示各文件系统;
    • -m :以 MBytes 的容量显示各文件系统;
    • -h :以人们较易阅读的 GBytes, MBytes, KBytes 等格式自行显示;
    • -H :以 M=1000K 取代 M=1024K 的进位方式;
    • -T :显示文件系统类型, 连同该 partition 的 filesystem 名称 (例如 ext3) 也列出;
    • -i :不用硬盘容量,而以 inode 的数量来显示

    测试:

    # 将系统内所有的文件系统列出来!
    # 在 Linux 底下如果 df 没有加任何选项
    # 那么默认会将系统内所有的 (不含特殊内存内的文件系统与 swap) 都以 1 Kbytes 的容量来列出来!
    [root@YKY /]# df
    Filesystem     1K-blocks    Used Available Use% Mounted on
    devtmpfs         1929728       0   1929728   0% /dev
    tmpfs            1940088       4   1940084   1% /dev/shm
    tmpfs            1940088     456   1939632   1% /run
    tmpfs            1940088       0   1940088   0% /sys/fs/cgroup
    /dev/vda1       41152812 2658820  36590284   7% /
    tmpfs             388020       0    388020   0% /run/user/0
    

    在这里插入图片描述
    将容量结果以易读的容量格式显示出来

    # 将容量结果以易读的容量格式显示出来
    [root@YKY /]# df -h
    Filesystem      Size  Used Avail Use% Mounted on
    devtmpfs        1.9G     0  1.9G   0% /dev
    tmpfs           1.9G  4.0K  1.9G   1% /dev/shm
    tmpfs           1.9G  456K  1.9G   1% /run
    tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
    /dev/vda1        40G  2.6G   35G   7% /
    tmpfs           379M     0  379M   0% /run/user/0
    

    在这里插入图片描述

    将系统内的所有特殊文件格式及名称都列出来

    #将系统内的所有特殊文件格式及名称都列出来
    [root@YKY /]# df -aT
    Filesystem     Type       1K-blocks   Used Available Use% Mounted on
    sysfs         sysfs               0       0         0    - /sys
    proc           proc                0       0         0    - /proc
    devtmpfs       devtmpfs       889100       0    889100   0% /dev
    securityfs     securityfs          0       0         0    - /sys/kernel/security
    tmpfs         tmpfs          899460     708    898752   1% /dev/shm
    devpts         devpts              0       0         0    - /dev/pts
    tmpfs         tmpfs          899460     496    898964   1% /run
    tmpfs         tmpfs          899460       0    899460   0% 
    ......
    

    将 /etc 底下的可用的磁盘容量以易读的容量格式显示

    # 将 /etc 底下的可用的磁盘容量以易读的容量格式显示
    [root@YKY /]# df -h /etc
    Filesystem     Size Used Avail Use% Mounted on
    /dev/vda1       40G  6.3G   32G  17% /
    

    3、du

    Linux du命令也是查看使用空间的,但是与df命令不同的是Linux du命令是对文件和目录磁盘使用的空间的查看,还是和df命令有一些区别的,这里介绍Linux du命令。

    语法:

    du [-ahskm] 文件或目录名称
    

    选项与参数:

    • -a :列出所有的文件与目录容量,因为默认仅统计目录底下的文件量而已。
    • -h :以人们较易读的容量格式 (G/M) 显示;
    • -s :列出总量而已,而不列出每个各别的目录占用容量;
    • -S :不包括子目录下的总计,与 -s 有点差别。
    • -k :以 KBytes 列出容量显示;
    • -m :以 MBytes 列出容量显示;

    测试:

    只列出当前目录下的所有文件夹容量

    # 只列出当前目录下的所有文件夹容量(包括隐藏文件夹):
    # 直接输入 du 没有加任何选项时,则 du 会分析当前所在目录的文件与目录所占用的硬盘空间。
    [root@YKY home]# du
    16	./redis
    8	./www/.oracle_jre_usage  # 包括隐藏文件的目录
    24	./www
    48	.                        # 这个目录(.)所占用的总量
    

    在这里插入图片描述
    将文件的容量也列出来

    # 将文件的容量也列出来
    [root@YKY home]# du -a
    4./redis/.bash_profile
    64	./yky/apache-tomcat-9.0.37/logs
    17308	./yky/apache-tomcat-9.0.37
    10952	./yky/apache-tomcat-9.0.37.tar.gz
    187456	./yky
    363612	.
    

    检查根目录底下每个目录所占用的容量

    # 检查根目录底下每个目录所占用的容量
    [root@kuangshen home]# du -sm /*
    0/bin
    146/boot
    .....中间省略....
    0/proc
    .....中间省略....
    1/tmp
    3026/usr  # 系统初期最大就是他了啦!
    513/var
    2666/www
    

    在这里插入图片描述

    通配符 * 来代表每个目录。
    与 df 不一样的是,du 这个命令其实会直接到文件系统内去搜寻所有的文件数据。

    4、磁盘挂载与卸除

    根文件系统之外的其他文件要想能够被访问,都必须通过“关联”至根文件系统上的某个目录来实现,此关联操作即为“挂载”,此目录即为“挂载点”,解除此关联关系的过程称之为“卸载”
    Linux 的磁盘挂载使用mount命令,卸载使用umount命令。

    磁盘挂载语法:

    mount [-t 文件系统] [-L Label名] [-o 额外选项] [-n] 装置文件名 挂载点
    

    测试:

    # 将 /dev/hdc6 挂载到 /mnt/hdc6 上面!
    [root@www ~]# mkdir /mnt/hdc6
    [root@www ~]# mount /dev/hdc6 /mnt/hdc6
    [root@www ~]# df
    Filesystem           1K-blocks     Used Available Use% Mounted on
    /dev/hdc6              1976312     42072   1833836   3% /mnt/hdc6
    

    磁盘卸载命令 umount 语法:

    umount [-fn] 装置文件名或挂载点
    

    选项与参数:

    • -f :强制卸除!可用在类似网络文件系统 (NFS) 无法读取到的情况下;
    • -n :不升级 /etc/mtab 情况下卸除。

    卸载/dev/hdc6

    [root@www ~]# umount /dev/hdc6
    

    三、进程管理

    1. 进程两种存在:
      1. 前台
      2. 后台
    2. 每个进程都有自己的id
    3. 每个进程都有一个父进程

    命令:

    ps -xxx:查看当前系统的进程
    
    • -a:显示当前终端运行的所有进程信息
    • -u:用户信息显示进程
    • -x:显示后台进程参数 ps
    • -aux:查看所有进程 ps
    • -aux|grep redis…查看redis的相关进程 ps
    • -ef 查看父进程的信息
      在这里插入图片描述
      在这里插入图片描述

    查看进程树:

    pstree
    
    • -p:显示父id
    • -u:显示用户组
      在这里插入图片描述

    结束进程:

    kill -9 进程id
    

    在这里插入图片描述

    展开全文
  • Linux进程管理和日志管理
  • Linux进程管理与调度-之-目录导航

    万次阅读 多人点赞 2016-05-19 19:57:29
    日期 内核版本 架构 作者 GitHub CSDN 2016-05-19 Linux-4.5 ... Linux进程管理与调度 进程的描述 CSDN GitHub Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一) study/kernel/p
    日期内核版本架构作者GitHubCSDN
    2016-07-21Linux-4.6X86 & armgatiemeLinuxDeviceDriversLinux进程管理与调度

    1 项目链接


    项目描述
    KernelInKernel一个运行在linux上的小巧内核, 修改了linux-kernel的start_kernel以启动我们自己的内核, 基于jserv/kernel-in-kernel(基于linux-4.1.0)和mengning/mykernel(基于linux-3.9.4), 适合学习和研究调度算法
    Linux进程管理与调度CSDN博客–Linux进程管理与调度
    LDD-LinuxDeviceDrivers与CSDN博客同步更新, 但是除了包含博客的内容, 还包含了一些以驱动方式实现的实验代码

    2 进程的描述


    CSDNGitHub
    Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一)study/kernel/01-process/01-task/01-task_struct
    Linux的命名空间详解–Linux进程的管理与调度(二)study/kernel/01-process/01-task/02-namespace
    Linux进程ID号–Linux进程的管理与调度(三)study/kernel/01-process/01-task/03-pid

    3 进程的创建


    CSDNGitHub
    Linux下的进程类别(内核线程、轻量级进程和用户进程)以及其创建方式–Linux进程的管理与调度(四) study/kernel/01-process/02-create/01-duplicate
    Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)study/kernel/01-process/02-create/02-idel
    Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六)study/kernel/01-process/02-create/03-init
    Linux下2号进程的kthreadd–Linux进程的管理与调度(七)study/kernel/01-process/02-create/04-kthreadd
    Linux下进程的创建过程分析(_do_fork/do_fork详解)–Linux进程的管理与调度(八)study/kernel/01-process/02-create/05-do_fork
    Linux进程内核栈与thread_info结构详解–Linux进程的管理与调度(九)study/kernel/01-process/02-create/06-thread_info
    Linux内核线程kernel thread详解–Linux进程的管理与调度(十)study/kernel/01-process/02-create/07-kernel_thead

    4 进程的加载与运行


    CSDNGitHub
    Linux进程启动过程分析do_execve(可执行程序的加载和运行)—Linux进程的管理与调度(十一)study/kernel/01-process/03-execute/01-do_execve
    LinuxELF文件格式详解–Linux进程的管理与调度(十二)study/kernel/01-process/03-execute/02-elf
    ELF文件的加载过程(load_elf_binary函数详解)–Linux进程的管理与调度(十三)study/kernel/01-process/03-execute/03-load_elf_binary

    5 进程的退出


    CSDNGitHub
    Linux进程退出详解(do_exit)–Linux进程的管理与调度(十四))study/kernel/01-process/04-exit/01-do_exit

    6 进程的调度


    CSDNGitHub
    Linux进程调度器概述–Linux进程的管理与调度(十五)study/kernel/01-process/05-schedule/01-introduction
    Linux进程调度策略的发展和演变–Linux进程的管理与调度(十六)study/kernel/01-process/05-schedule/02-develop
    Linux进程调度器的设计–Linux进程的管理与调度(十七)study/kernel/01-process/05-schedule/03-design
    Linux核心调度器之周期性调度器scheduler_tick–Linux进程的管理与调度(十八)study/kernel/01-process/05-schedule/03-design/02-periodic_scheduler
    Linux进程核心调度器之主调度器–Linux进程的管理与调度(十九)study/kernel/01-process/05-schedule/03-design/03-main_scheduler
    Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)study/kernel/01-process/05-schedule/03-design/04-preempt
    Linux进程上下文切换过程context_switch详解–Linux进程的管理与调度(二十一)study/kernel/01-process/05-schedule/03-design/05-context_switch
    Linux进程优先级的处理–Linux进程的管理与调度(二十二)study/kernel/01-process/05-schedule/03-design/06-priority
    Linux唤醒抢占----Linux进程的管理与调度(二十三)study/kernel/01-process/05-schedule/03-design/07-wakeup

    #7 调度普通进程-完全公平调度器CFS

    CSDNGitHub
    Linux进程调度之CFS调度器概述–Linux进程的管理与调度(二十四)study/kernel/01-process/05-schedule/07-cfs/01-cfs/
    Linux CFS调度器之负荷权重load_weight–Linux进程的管理与调度(二十五)study/kernel/01-process/05-schedule/07-cfs/02-load_weight/
    Linux CFS调度器之虚拟时钟vruntime与调度延迟–Linux进程的管理与调度(二十六)study/kernel/01-process/05-schedule/07-cfs/03-vruntime/
    Linux CFS调度器之队列操作–Linux进程的管理与调度(二十七)study/kernel/01-process/05-schedule/07-cfs/04-queue/
    Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程–Linux进程的管理与调度(二十八)study/kernel/01-process/05-schedule/07-cfs/05-pick_next/
    Linux CFS调度器之task_tick_fair处理周期性调度器–Linux进程的管理与调度(二十九)study/kernel/01-process/05-schedule/07-cfs/06-task_tick_fair/
    Linux CFS调度器之唤醒抢占–Linux进程的管理与调度(三十)study/kernel/01-process/05-schedule/07-cfs/07-task_new_fair/
    Linux CFS调度器之唤醒WAKE_AFFINE 机制–Linux进程的管理与调度(三十一)study/kernel/01-process/05-schedule/07-cfs/08-wake_affine

    7 公众号


    工作以后,很长时间,没写博客了。近期准备重新拾起来,知识是无界的,我最喜欢的就是把技术当笔记一样分享出来跟大家一起讨论,一些思考。

    近期开了公众号和知乎, 刚开始运营,欢迎大家多多支持。

    推荐大家关注下我的公众号,内核干货,谢谢。

    后期所有博文都将在这些平台同步推送,大家选择自己关注的平台即可。当然推荐大家把公众号关注了,谢谢。

    CSDN公众号知乎自建站点
    kernel-csdn内核干货知乎oskernellsb
    kernel-csdn公众号 "内核干货"知乎在这里插入图片描述
    展开全文
  • Linux进程管理(一)进程数据结构

    千次阅读 2019-10-27 16:59:46
    Linux进程管理(一)进程数据结构 文章目录Linux进程管理(一)进程数据结构双向链表任务ID信号处理进程状态进程调度运行统计信息进程亲缘关系内存管理文件与文件系统进程内核栈栈结构current宏 Linux内核中使用 ...
  • 一文教你了解Linux进程管理

    千次阅读 多人点赞 2020-10-24 13:03:49
    Linux进程管理一. 什么是进程和程序二. 查看进程——ps,top,pstree三. 进程的启动方式四.进程的控制五.实训任务 一. 什么是进程和程序 进程:开始执行但是还没有结束的程序的实例 程序:包含可执行代码的文件 ...
  • linux进程管理详解(好书)linux进程管理详解(好书)linux进程管理详解(好书)linux进程管理详解(好书)
  • 2020 最新 Linux进程管理题目三

    千次阅读 2020-11-03 16:31:13
    Linux进程管理实训题目 (1)输入ps命令,分别就不带选项和带选项-a、-e、-f、u、x分析输出结果,明确各字段的含义 (2)输入ps –ef命令从后向前找出各自的父进程,直至1号进程,画出相关进程的族系关系图 (3)...
  • Linux进程管理 一. 什么是linux的进程 1. linux进程的概述 (1)只要执行程序,就会有进程出现,并且分配对应的ID(PID)进程号码 在操作系统中,所有可以执行的程序与命令都会产生进程。 只是有些程序和命令非常...
  • 本文为IBM RedBook的Linux Performanceand Tuning Guidelines的1.1节的翻译原文地址:http://www.redbooks.ibm.com/redpapers/pdfs/redp4285.pdf原文作者:Eduardo Ciliendo, Takechika ...1.1 Linux进程管理进程管理
  • Linux进程管理常用命令及监控工具

    万次阅读 2018-06-05 10:30:51
    Linux进程管理常用命令 进程的管理命令有: pstree、ps、pidof、pgrep、pkill、pmap、kill、killall、job、bg、fg等 进程的管理工具: top、htop、vmstat、dstat、iostat、glances等 进程管理命令: pstree...
  • linux进程管理子系统简要分析

    千次阅读 2016-05-05 19:21:39
    Linux进程管理: 进程与程序: 程序:存放在磁盘上的一系列代码和数据的可执行映像,是一个静止的实体。 进程:是一个执行中的程序,它是动态的实体 进程四要素: 1. 有一段程序供其执行,这段程序不一定是...
  • Linux进程管理之进程树pstree

    千次阅读 2018-11-17 22:00:12
    pstree [选项]:更加直观的来看进程信息 选项 作用 -p 显示进程的PID -u 显示进程的所属用户 案例1: 请以树状的形式显示进程的PID pstree -p 案例2: 请以树状的形式显示进程的用户PID pstree -u ...
  • Linux进程管理实验

    千次阅读 2018-05-29 15:20:56
    ②通过Linux管道通信机制,消息队列通信机制,共享内存通信机制使用,加深对不同类型进程通信方式理解 ③加深对信号量同步机制的理解 2.实验内容: ①实现一个模拟shell:编写三个不同的程序:cmd1.c, cmd2.c,...
  • linux进程管理命令

    千次阅读 2019-02-28 13:21:21
    1、inux进程信息 1.1.ps 命令:报告程序状况 ps -A 显示所有程序 ps -ef 显示所有程序,并以ascii字符显示树状结构,表达程序间的相互关系。 ps -ef | grep xxx 在上面基础上添加查找关键字并显示出来。 1.2...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 463,881
精华内容 185,552
关键字:

linux进程管理

linux 订阅