彩世界开奖app官网-彩世界平台官方网址(彩票平台)
做最好的网站
来自 彩世界开奖app官网 2019-12-06 21:53 的文章
当前位置: 彩世界开奖app官网 > 彩世界开奖app官网 > 正文

Linux内核学习笔记——进程管理彩世界开奖app官网

2.Linux操作系统的进程组织

(1)什么是进程

  进程是处于执行期的程序以及它所包含的所有资源的总称,包括虚拟处理器,虚拟空间,寄存器,堆栈,全局数据段等。

  在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制(Process Control Block,简称PCB)。PCB中包含了很多重要的信息,供系统调度和进程本身执行使用。所有进程的PCB都存放在内核空间中。PCB中最重要的信息就是进程PID,内核通过这个PID来唯一标识一个进程。PID可以循环使用,最大值是32768。init进程的pid为1,其他进程都是init进程的后代。

  除了进程控制块(PCB)以外,每个进程都有独立的内核堆栈(8k),一个进程描述符结构,这些数据都作为进程的控制信息储存在内核空间中;而进程的用户空间主要存储代码和数据。

查看进程:

彩世界开奖app官网 1

 

(2)进程创建

  进程是通过调用::fork(),::vfork()【只复制task_struct和内核堆栈,所以生成的只是父进程的一个线程(无独立的用户空间)。】和::clone()【功能强大,带了许多参数。::clone()可以让你有选择性的继承父进程的资源,既可以选择像::vfork()一样和父进程共享一个虚拟空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。】系统调用创建新进程。在内核中,它们都是调用do_fork实现的。传统的fork函数直接把父进程的所有资源复制给子进程。而Linux的::fork()使用写时拷贝页实现,也就是说,父进程和子进程共享同一个资源拷贝,只有当数据发生改变时,数据才会发生复制。通常的情况,子进程创建后会立即调用exec(),这样就避免复制父进程的全部资源。

    #fork():父进程的所有数据结构都会复制一份给子进程(写时拷贝页)。当执行fork()函数后,会生成一个子进程,子进程的执行从fork()的返回值开始,且代码继续往下执行

以下代码中,使用fork()创建了一个子进程。返回值pId有两个作用:一是判断fork()是否正常执行;二是判断fork()正常执行后如何区分父子进程。

 1 #代码示例:
 2 #include <stdio.h>  
 3 #include <stdlib.h>  
 4 #include <unistd.h>  
 5   
 6 int main (int argc, char ** argv) {  
 7     int flag = 0;  
 8     pid_t pId = fork();  
 9     if (pId == -1) {  
10         perror("fork error");  
11         exit(EXIT_FAILURE);  
12     } else if (pId == 0) {  
13         int myPid = getpid();  
14         int parentPid = getppid();  
15           
16         printf("Child:SelfID=%d ParentID=%d n", myPid, parentPid);  
17         flag = 123;  
18         printf("Child:flag=%d %p n", flag, &flag);  
19         int count = 0;  
20         do {  
21             count  ;  
22             sleep(1);  
23             printf("Child count=%d n", count);  
24             if (count >= 5) {  
25                 break;  
26             }  
27         } while (1);  
28         return EXIT_SUCCESS;  
29     } else {  
30         printf("Parent:SelfID=%d MyChildPID=%d n", getpid(), pId);  
31         flag = 456;  
32         printf("Parent:flag=%d %p n", flag, &flag); // 连地址都一样,说明是真的完全拷贝,但值已经是不同的了..  
33         int count = 0;  
34         do {  
35             count  ;  
36             sleep(1);  
37             printf("Parent count=%d n", count);  
38             if (count >= 2) {  
39                 break;  
40             }  
41         } while (1);  
42     }  
43       
44     return EXIT_SUCCESS;  
45 } 

 

(3)进程撤销

  进程通过调用exit()退出执行,这个函数会终结进程并释放所有的资源。父进程可以通过wait4()查询子进程是否终结。进程退出执行后处于僵死状态,直到它的父进程调用wait()或者waitpid()为止。父进程退出时,内核会指定线程组的其他进程或者init进程作为其子进程的新父进程。当进程接收到一个不能处理或忽视的信号时,或当在内核态产生一个不可恢复的CPU异常而内核此时正代表该进程在运行,内核可以强迫进程终止。

 

(4)进程管理

  内核把进程信息存放在叫做任务队列(task list)的双向循环链表中(内核空间)。链表中的每一项都是类型为task_struct,称为进程描述符结构(process descriptor),包含了一个具体进程的所有信息,包括打开的文件,进程的地址空间,挂起的信号,进程的状态等。

彩世界开奖app官网 2

 

  Linux通过slab分配器分配task_struct,这样能达到对象复用和缓存着色(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗)。

彩世界开奖app官网 3

struct task_struct 
{
volatile long state;
pid_t pid;
unsigned long timestamp;
unsigned long rt_priority;
struct mm_struct *mm, *active_mm
}

 

对于向下增长的栈来说,只需要在栈底(对于向上增长的栈则在栈顶)创建一个新的结构struct thread_info,使得在汇编代码中计算其偏移量变得容易。

#在x86上,thread_info结构在文件<asm/thread_info.h>中定义如下:
struct thread_info{
             struct task_struct              *任务
             struct exec_domain              *exec_domain;
             unsigned long                   flags;
             unsigned long                   status;
             __u32                           cpu;
             __s32                           preempt_count;
             mm_segment_t                    addr_limit;
             struct restart_block            restart_block;
             unsigned long                   previous_esp;
             _u8                             supervisor_stack[0];
    };

 

  内核把所有处于TASK_RUNNING状态的进程组织成一个可运行双向循环队列。调度函数通过扫描整个可运行队列,取得最值得执行的进程投入执行。避免扫描所有进程,提高调度效率。

#进程调度使用schedule()函数来完成,下面我们从分析该函数开始,代码如下:
1 asmlinkage __visible void __sched schedule(void)
2 {
3     struct task_struct *tsk = current;
4 
5     sched_submit_work(tsk);
6     __schedule();
7 }
8 EXPORT_SYMBOL(schedule);
#在第4段进程调度中将具体讲述功能实现

 

(5)进程内核堆栈

  Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:thread_info和进程的内核堆栈。

  进程处于内核态时使用不同于用户态堆栈,内核控制路径所用的堆栈很少,因此对栈和描述符来说,8KB足够了。

彩世界开奖app官网 4

 

进程终结

当一个进程终结时必须释放它所占有的资源。进程主动终结发生在进程调用exit()系统调用时,当然它还有可能被动终结。

  • 删除进程描述符:在调用do_exit()之后,尽管线程已经僵死不能再运行了,但系统还保留了它的进程描述符,在父进程获得已终结的子进程的信息或通知内核它不关注那些信息后,子进程的task_struct结构才释放。
  • 孤儿进程造成的进退维谷:由于进程退出时需要父进程通知父进程释放子进程的task_struct,如果一个进程找不到父进程就会在退出时永远处于僵死状态。因此要在父进程退出时为每一个子进程找到一个新的父亲,方法是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init做它们的父进程。

4.4 撤销进程

进程结束时必须通知内核,以便内核释放进程所拥有的资源,包括内存,打开文件,信号量等。

进程终止有8种方式:


正常终止: 1、从main返回 2、调用exit(做一些清理并结束进程) 3、调用_exit或_Exit(直接结束进程)(基于exit_group()系统调用) 4、最后一个线程从其启动例程返回 5、最后一个线程调用pthread_exit(基于exit()系统调用) 异常终止: 6、调用abort 7、接收到一个信号并终止 8、最后一个线程对取消请求作出响应


彩世界开奖app官网 5

两个进程终止的系统调用:

1、exit_group(),终止整个线程组,也适用于单线程进程。c库函数exit()基于此系统调用 2、exit(),终止某一个线程,而不管该线程所属线程组中的所有其他线程。linux线程库函数pthread_exit()基于此系统调用

//  kernel/exit.cfastcall NORET_TYPE void do_exit(long code){ struct task_struct *tsk = current; int group_dead; profile_task_exit(tsk); if (unlikely(in_interrupt()))  panic("Aiee, killing interrupt handler!"); if (unlikely(!tsk->pid))  panic("Attempted to kill the idle task!"); if (unlikely(tsk->pid == 1))  panic("Attempted to kill init!"); if (tsk->io_context)  exit_io_context(); if (unlikely(current->ptrace & PT_TRACE_EXIT)) {  current->ptrace_message = code;  ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP); } /* 更新状态,进程正在退出*/ tsk->flags |= PF_EXITING; del_timer_sync(&tsk->real_timer); if (unlikely(in_atomic()))  printk(KERN_INFO "note: %s[%d] exited with preempt_count %dn",    current->comm, current->pid,    preempt_count()); acct_update_integrals(); update_mem_hiwater(); group_dead = atomic_dec_and_test(&tsk->signal->live); if (group_dead)  acct_process(code); /* 解除对内存,信号量,文件系统,打开文件,命名空间等的引用,非共享则删除 */ exit_mm(tsk); exit_sem(tsk); __exit_files(tsk); __exit_fs(tsk); exit_namespace(tsk); exit_thread(); exit_keys(tsk); if (group_dead && tsk->signal->leader)  disassociate_ctty(1); module_put(tsk->thread_info->exec_domain->module); if (tsk->binfmt)  module_put(tsk->binfmt->module); /* exit_code,系统调用参数(正常终止)或内核提供的错误码(异常终止)*/ tsk->exit_code = code; /* 更新亲属关系,子进程将被兄弟进程或init收养   * 是否需要向父进程发送SIGCHLD信号  * release_task()回收进程其他数据结构占用的内存  * 进程EXIT_DEAD或EXIT_ZOMBIE*/ exit_notify(tsk);#ifdef CONFIG_NUMA mpol_free(tsk->mempolicy); tsk->mempolicy = NULL;#endif  BUG_ON(!(current->flags & PF_DEAD)); /* 进程调度,一去不回 */ schedule(); BUG(); /* Avoid "noreturn function does return".  */ for (;;) ;}

至此,进程已死,永不会复活,但其尸体(某些数据结构)还在,对僵死进程的处理有两种可能的方式:
如果父进程不需要接收来自子进程的信号,就调用do_exit(); 如果已经给父进程发送了一个信号,就调用wait4()或waitpid()系统调用。 后一种情况下,release_task()函数将回收进程描述符所占用的内存空间; 而在前一种情况下,内存的回收将由进程调度程序来完成。

1.Linux操作系统的简易介绍

  Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。    

(1)内核

  内核是操作系统的核心,具有很多最基本功能,如虚拟内存、多任务、共享库、需求加载、可执行程序和TCP/IP网络功能。Linux内核的模块分为以下几个部分:存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信、系统的初始化和系统调用等。

(2)shell

  shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行,是一个命令解释器。另外,shell编程语言具有普通编程语言的很多特点,用这种编程语言编写的shell程序与其他应用程序具有同样的效果。

(3)文件系统

  文件系统是文件存放在磁盘等存储设备上的组织方法。Linux系统能支持多种目前流行的文件系统,如EXT2、EXT3、FAT、FAT32、VFAT和ISO9660。

(4)应用程序

  标准的Linux系统一般都有一套都有称为应用程序的程序集,它包括文本编辑器、编程语言、XWindow、办公套件、Internet工具和数据库等。

 

进程描述符及任务结构

  • 任务队列:存放进程列表的双向循环链表
  • task_struct:进程描述符,包含一个具体进程的所有信息。2.6以后的版本通过slab动态生成task_struct。
  • thread_info:线程描述符,

1.2 什么是线程?

有时候,一个进程希望有多个执行流,如一款麻将游戏,三个由电脑控制的人都应被看做是“独立思考的”,它们需要并行工作。但它们又不是完全无关的,不能设计成单独的进程。所以就需要比进程更小的单位,它们独立被调度,又共享一些资源。

5.对于Linux操作系统进程模型的一些个人看法

  有一个形象的比喻:想象一位知识渊博、经验丰富的工程建筑设计师正在为一个公司设计总部。他有公司建筑的设计图,有所需的建筑材料和工具:水泥、钢筋、木板、挖掘机、吊升机、石钻头等。在这个比喻中,设计图就是程序(即用适当形式描述的算法),工程建筑师就是处理器(CPU),而建筑的各种材料就是输入数据。进程就是建筑工程设计师阅读设计图、取来各种材料和工具以及管理工人员工和分配资源、最后施工等一系列动作的总和,在过程中工程建筑师还需要遵循许多设计的规范和理念(模型),最后完成的公司总部就是软件或者可以实现某种功能的源代码。

  这里说明的是进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。那么Linux操作系统进程模型就是活动的规范,规范的出现创新让许多实现过程更加系统完整、安全可靠、速度效率等。

  就像人类基于理论实践伟大的工程设计智慧经验结晶,Linux操作系统是系统、效率、安全的,而且通过商业公司、庞大的社区群体、操作系统爱好者是在往前改善的,但如果有一天Linux操作系统闭源了,只有国内开放了源代码,还尚未掌握核心技术,卡住脖子怎么办?我们不能拥有完完全全拿来即用的心态,还需扎实掌握基础知识,提高自我创新意识。对于Linux操作系统进程模型,深入理解它,你会发现在Linux操作系统的应用实践上会愈加效率,同时通过它你可以实现更多好玩的操作。

定义

进程就是处于执行期的程序。实际上,进程就是正在执行代码的实际结果。
线程是在进程中活动的对象,每个线程都拥有独立的程序计数器,进程栈以及一组进程寄存器。内核的调度对象是线程,而不是
进程。

2.1 如何获得进程描述符

内核必须快速得到当前进程task_struct,current()宏可以得到当前进程的task_struct指针,它是如何实现的? Linux内核用了一个小技巧,在8KB内核栈配置下(内核栈可以配置为8KB或4KB,详见《深入理解linux内核》“多种类型的内核栈”),在该进程内核栈的顶端(低地址)放一个小的结构体thread_info(52字节),再用thread_info.task指向task_struct。

// include/linux/sched.h union thread_union{    struct thread_info thread_info;    unsigned long stack[2048];}

彩世界开奖app官网 6
esp是CPU的栈指针,用来存放栈顶单元的地址,可以直接得到。(esp & 0xffff e000)可以得到thread_info的指针,thread_info.task就是task_struct的指针。
注: 1、task是struct thread_info中的第一个元素,应该在最低地址,图中的画法是错误的; 2、current应该指向进程描述符,而不是struct thread_info。 更多细节: 2.6以前的内核,直接task_struct放在内核栈的尾端,2.6以后使用slab分配器分配task_struct,所以成了我们看到的这样子;对比PPC这部分的实现,PPC使用r2寄存器存储task_struct指针,x86属于廉价处理器,寄存器数量有限,无法做到这一点。

4.Linux操作系统的进程调度

  毋庸置疑,我们使用schedule()函数来完成进程调度,接下来就来看看进程调度的代码以及实现过程吧。

1 asmlinkage __visible void __sched schedule(void)
2 {
3     struct task_struct *tsk = current;
4 
5     sched_submit_work(tsk);
6     __schedule();
7 }
8 EXPORT_SYMBOL(schedule);

 

  第3行获取当前进程描述符指针,存放在本地变量tsk中。第6行调用__schedule(),代码如下(kernel/sched/core.c):

彩世界开奖app官网 7彩世界开奖app官网 8

 1 static void __sched __schedule(void)
 2 {
 3     struct task_struct *prev, *next;
 4     unsigned long *switch_count;
 5     struct rq *rq;
 6     int cpu;
 7 
 8 need_resched:
 9     preempt_disable();
10     cpu = smp_processor_id();
11     rq = cpu_rq(cpu);
12     rcu_note_context_switch(cpu);
13     prev = rq->curr;
14 
15     schedule_debug(prev);
16 
17     if (sched_feat(HRTICK))
18         hrtick_clear(rq);
19 
20     /*
21      * Make sure that signal_pending_state()->signal_pending() below
22      * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
23      * done by the caller to avoid the race with signal_wake_up().
24      */
25     smp_mb__before_spinlock();
26     raw_spin_lock_irq(&rq->lock);
27 
28     switch_count = &prev->nivcsw;
29     if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
30         if (unlikely(signal_pending_state(prev->state, prev))) {
31             prev->state = TASK_RUNNING;
32         } else {
33             deactivate_task(rq, prev, DEQUEUE_SLEEP);
34             prev->on_rq = 0;
35 
36             /*
37              * If a worker went to sleep, notify and ask workqueue
38              * whether it wants to wake up a task to maintain
39              * concurrency.
40              */
41             if (prev->flags & PF_WQ_WORKER) {
42                 struct task_struct *to_wakeup;
43 
44                 to_wakeup = wq_worker_sleeping(prev, cpu);
45                 if (to_wakeup)
46                     try_to_wake_up_local(to_wakeup);
47             }
48         }
49         switch_count = &prev->nvcsw;
50     }
51 
52     if (prev->on_rq || rq->skip_clock_update < 0)
53         update_rq_clock(rq);
54 
55     next = pick_next_task(rq, prev);
56     clear_tsk_need_resched(prev);
57     clear_preempt_need_resched();
58     rq->skip_clock_update = 0;
59 
60     if (likely(prev != next)) {
61         rq->nr_switches  ;
62         rq->curr = next;
63           *switch_count;
64 
65         context_switch(rq, prev, next); /* unlocks the rq */
66         /*
67          * The context switch have flipped the stack from under us
68          * and restored the local variables which were saved when
69          * this task called schedule() in the past. prev == current
70          * is still correct, but it can be moved to another cpu/rq.
71          */
72         cpu = smp_processor_id();
73         rq = cpu_rq(cpu);
74     } else
75         raw_spin_unlock_irq(&rq->lock);
76 
77     post_schedule(rq);
78 
79     sched_preempt_enable_no_resched();
80     if (need_resched())
81         goto need_resched;
82 }

static void __sched __schedule(void)

 

  第9行禁止内核抢占。第10行获取当前的cpu号。第11行获取当前cpu的进程运行队列。第13行将当前进程的描述符指针保存在prev变量中。第55行将下一个被调度的进程描述符指针存放在next变量中。第56行清除当前进程的内核抢占标记。第60行判断当前进程和下一个调度的是不是同一个进程,如果不是的话,就要进行调度。第65行,对当前进程和下一个进程的上下文进行切换(调度之前要先切换上下文)。下面看看该函数(kernel/sched/core.c):

彩世界开奖app官网 9彩世界开奖app官网 10

 1 context_switch(struct rq *rq, struct task_struct *prev,
 2            struct task_struct *next)
 3 {
 4     struct mm_struct *mm, *oldmm;
 5 
 6     prepare_task_switch(rq, prev, next);
 7 
 8     mm = next->mm;
 9     oldmm = prev->active_mm;
10     /*
11      * For paravirt, this is coupled with an exit in switch_to to
12      * combine the page table reload and the switch backend into
13      * one hypercall.
14      */
15     arch_start_context_switch(prev);
16 
17     if (!mm) {
18         next->active_mm = oldmm;
19         atomic_inc(&oldmm->mm_count);
20         enter_lazy_tlb(oldmm, next);
21     } else
22         switch_mm(oldmm, mm, next);
23 
24     if (!prev->mm) {
25         prev->active_mm = NULL;
26         rq->prev_mm = oldmm;
27     }
28     /*
29      * Since the runqueue lock will be released by the next
30      * task (which is an invalid locking op but in the case
31      * of the scheduler it's an obvious special-case), so we
32      * do an early lockdep release here:
33      */
34 #ifndef __ARCH_WANT_UNLOCKED_CTXSW
35     spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
36 #endif
37 
38     context_tracking_task_switch(prev, next);
39     /* Here we just switch the register state and the stack. */
40     switch_to(prev, next, prev);
41 
42     barrier();
43     /*
44      * this_rq must be evaluated again because prev may have moved
45      * CPUs since it called schedule(), thus the 'rq' on its stack
46      * frame will be invalid.
47      */
48     finish_task_switch(this_rq(), prev);
49 }

context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)

 

  上下文切换一般分为两个,一个是硬件上下文切换(指的是cpu寄存器,要把当前进程使用的寄存器内容保存下来,再把下一个程序的寄存器内容恢复),另一个是切换进程的地址空间(说白了就是程序代码)。进程的地址空间(程序代码)主要保存在进程描述符中的struct mm_struct结构体中,因此该函数主要是操作这个结构体。第17行如果被调度的下一个进程地址空间mm为空,说明下个进程是个线程,没有独立的地址空间,共用所属进程的地址空间,因此第18行将上个进程所使用的地址空间active_mm指针赋给下一个进程的该域,下一个进程也使用这个地址空间。第22行,如果下个进程地址空间不为空,说明下个进程有自己的地址空间,那么执行switch_mm切换进程页表。第40行切换进程的硬件上下文。 switch_to函数代码如下(arch/x86/include/asm/switch_to.h):

彩世界开奖app官网 11彩世界开奖app官网 12

 1 __visible __notrace_funcgraph struct task_struct *
 2 __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
 3 {
 4     struct thread_struct *prev = &prev_p->thread,
 5                  *next = &next_p->thread;
 6     int cpu = smp_processor_id();
 7     struct tss_struct *tss = &per_cpu(init_tss, cpu);
 8     fpu_switch_t fpu;
 9 
10     /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
11 
12     fpu = switch_fpu_prepare(prev_p, next_p, cpu);
13 
14     /*
15      * Reload esp0.
16      */
17     load_sp0(tss, next);
18 
19     /*
20      * Save away %gs. No need to save %fs, as it was saved on the
21      * stack on entry.  No need to save %es and %ds, as those are
22      * always kernel segments while inside the kernel.  Doing this
23      * before setting the new TLS descriptors avoids the situation
24      * where we temporarily have non-reloadable segments in %fs
25      * and %gs.  This could be an issue if the NMI handler ever
26      * used %fs or %gs (it does not today), or if the kernel is
27      * running inside of a hypervisor layer.
28      */
29     lazy_save_gs(prev->gs);
30 
31     /*
32      * Load the per-thread Thread-Local Storage descriptor.
33      */
34     load_TLS(next, cpu);
35 
36     /*
37      * Restore IOPL if needed.  In normal use, the flags restore
38      * in the switch assembly will handle this.  But if the kernel
39      * is running virtualized at a non-zero CPL, the popf will
40      * not restore flags, so it must be done in a separate step.
41      */
42     if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
43         set_iopl_mask(next->iopl);
44 
45     /*
46      * If it were not for PREEMPT_ACTIVE we could guarantee that the
47      * preempt_count of all tasks was equal here and this would not be
48      * needed.
49      */
50     task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
51     this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);
52 
53     /*
54      * Now maybe handle debug registers and/or IO bitmaps
55      */
56     if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
57              task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
58         __switch_to_xtra(prev_p, next_p, tss);
59 
60     /*
61      * Leave lazy mode, flushing any hypercalls made here.
62      * This must be done before restoring TLS segments so
63      * the GDT and LDT are properly updated, and must be
64      * done before math_state_restore, so the TS bit is up
65      * to date.
66      */
67     arch_end_context_switch(next_p);
68 
69     this_cpu_write(kernel_stack,
70           (unsigned long)task_stack_page(next_p)  
71           THREAD_SIZE - KERNEL_STACK_OFFSET);
72 
73     /*
74      * Restore %gs if needed (which is common)
75      */
76     if (prev->gs | next->gs)
77         lazy_load_gs(next->gs);
78 
79     switch_fpu_finish(next_p, fpu);
80 
81     this_cpu_write(current_task, next_p);
82 
83     return prev_p;
84 }

__visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)

  该函数主要是对刚切换过来的新进程进一步做些初始化工作。比如第34将该进程使用的线程局部存储段(TLS)装入本地cpu的全局描述符表。第84行返回语句会被编译成两条汇编指令,一条是将返回值prev_p保存到eax寄存器,另外一个是ret指令,将内核栈顶的元素弹出eip寄存器,从这个eip指针处开始执行,也就是上个函数第17行所压入的那个指针。一般情况下,被压入的指针是上个函数第20行那个标号1所代表的地址,那么从__switch_to函数返回后,将从标号1处开始运行。

  需要注意的是,对于已经被调度过的进程而言,从__switch_to函数返回后,将从标号1处开始运行;但是对于用fork(),clone()等函数刚创建的新进程(未调度过),将进入ret_from_fork()函数,因为do_fork()函数在创建好进程之后,会给进程的thread_info.ip赋予ret_from_fork函数的地址,而不是标号1的地址,因此它会跳入ret_from_fork函数。后边我们在分析fork系统调用的时候,就会看到。

 

PID

唯一的进程标志值。int类型,为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768,这个值最大可增加到400万。进程的PID存放在进程描述符中。

4.3 我们接下来从内核中走出来,站在应用程序员的角度看看进程

系统调用的返回值放在eax寄存器中:返回给子进程的值是0,返回给父进程的值是子进程的PID。这是UNIX的通用做法,应用开发者可以利用这一事实,使用基于PID值的条件语句,使子进程和父进程有不同的行为。如下所示:

#include <stdio.h>#include <bits/types.h>#include <unistd.h>#include <stdlib.h> int glob = 6;char buf[] = "a write to stdoutn";int main(void){ int var; pid_t pid; var = 88; if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)  printf("write errorn"); printf("before forkn"); if((pid = fork()) < 0){  printf("fork errorn"); } else if(pid == 0) {    /* child */  glob  ;  var  ; } else{                  /* parent */  sleep(2); } printf("pid = %d, glob = %d, var = %dn", getpid(), glob, var); exit(0);}

彩世界开奖app官网 13
这里引出另一个问题,就是上面两种不同的执行方式造成结果不同的原因。fork时,父进程数据空间复制到子进程中时,缓冲区也被复制到子进程中。 write是不带缓冲的,标准IO库(如printf)是带缓冲的。如果标准输出连接到终端设备,则它是行缓冲的,否则它是全缓冲的。(详见《UNIX环境高级编程》)

从用户态看来,子进程继承了父进程的(有些需要结合《UNIX环境高级编程》上下文才能看懂):


打开文件

实际用户ID、实际组ID、有效用户ID、有效组ID

附加组ID

进程组ID

会话ID

控制终端

设置用户ID标志和设置组ID标志

当前工作目录

根目录

文件模式创建屏蔽字

信号屏蔽和安排

针对任意打开文件描述符的在执行时关闭标志

环境

连接的共享存储段

存储映射

资源限制


父进程和子进程的区别是:


fork的返回值

进程ID不同

父进程ID

子进程的tms_utime、tms_stime、 tms_cutime以及tms_ustime均被设置为0

父进程设置的文件所不会被子进程继承

子进程的未处理的闹钟被清除

子进程的未处理信号集设置为空集


父子进程对打开文件的共享:


彩世界开奖app官网 14


注意区别于独立进程打开文件:


彩世界开奖app官网 15


6.参考资料

Contiki学习笔记:目录。

源码地址

脚本之家(

CSDN博客(

百度知道(

进程的两种虚拟机制

  1. 虚拟处理器:每个线程独有,不能共享
  2. 虚拟内存:同一个进程中的线程可以共享

4.2 如何创建进程

进程是在系统运行过程中动态创建的,例如:用户在shell中输入一条命令、程序执行fork或pthread_create等。

此时,进程如何创建呢?-->

fork系统调用,以前的做法是,子进程复制父进程所拥有的资源。但是很多情况下,子进程要做与父进程不同的事,所以子进程立即调用execve(),复制的数据立即丢弃,所以效率低。

后来引入了vfork系统调用,子进程共享其父进程的内存地址空间,并阻塞父进程的执行,一直到子进程退出或执行一个新的程序。

现在的fork引入了写时复制技术(copy-on-write) --> vfrok的优势不再,应避免使用。

此外,clone系统调用允许细致地控制子进程共享哪些父进程的数据,被用来实现轻量级进程。

下表列出了clone的共享标志:

// include/linux/sched.h /* * cloning flags: */#define CSIGNAL  0x000000ff /* signal mask to be sent at exit */#define CLONE_VM 0x00000100 /* set if VM shared between processes */#define CLONE_FS 0x00000200 /* set if fs info shared between processes */#define CLONE_FILES 0x00000400 /* set if open files shared between processes */#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */#define CLONE_THREAD 0x00010000 /* Same thread group? */#define CLONE_NEWNS 0x00020000 /* New namespace group? */#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */#define CLONE_DETACHED  0x00400000 /* Unused, ignored */#define CLONE_UNTRACED  0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */#define CLONE_STOPPED  0x02000000 /* Start in stopped state */ /* * List of flags we want to share for kernel threads, * if only because they are not used by them anyway. */#define CLONE_KERNEL    (CLONE_FS | CLONE_FILES | CLONE_SIGHAND)

fork()、clone()、vfrok()系统调用均使用do_fork()函数实现。

//  kernel/fork.c /* *  Ok, this is the main fork-routine. * * It copies the process, and if successful kick-starts * it and waits for it to finish using the VM if required. */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 pid = alloc_pidmap();//通过查找pidmap_array位图,为子进程分配新的PID if (pid < 0)  return -EAGAIN; /* 检查子进程是否要跟踪*/ if (unlikely(current->ptrace)) {  trace = fork_traceflag (clone_flags);  if (trace)   clone_flags |= CLONE_PTRACE; } /* 核心!复制父进程的task_struct,并申请了内核栈和thread_info */ p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid); /*  * Do this prior waking up the new thread - the thread pointer  * might get invalid after that point, if the thread exits quickly.  */ if (!IS_ERR(p)) {  struct completion vfork;  if (clone_flags & CLONE_VFORK) {   p->vfork_done = &vfork;   init_completion(&vfork);  }  /* 如果设置了CLONE_STOPPED标志,或要跟踪子进程,那么子进程被设置成TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号,在另一进程把子进程的状态恢复为TASK_RUNNING之前(通常是SIGCONT信号),子进程不得运行*/  if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {   /*    * We'll start up with an immediate SIGSTOP.    */   sigaddset(&p->pending.signal, SIGSTOP);   set_tsk_thread_flag(p, TIF_SIGPENDING);  }  /* 唤醒子进程,1)若父子进程在同一cpu且不能共享页表(CLONE_VM=0),则在运行队列中,把子进程插入在父进程前面,以避免不必要的写时复制开销;2)不同cpu或CLONE_VM=1,把子进程插入如今成运行队列的队尾 */  if (!(clone_flags & CLONE_STOPPED))   wake_up_new_task(p, clone_flags);  else   p->state = TASK_STOPPED;  /* 如果父进程被跟踪,则把子进程pid保存,以使祖父进程(debugger)获取 */  if (unlikely (trace)) {   current->ptrace_message = pid;   ptrace_notify ((trace << 8) | SIGTRAP);  }  /* vfrok要求父进程挂起,直到子进程结束或执行新的程序 */  if (clone_flags & CLONE_VFORK) {   wait_for_completion(&vfork);   if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))    ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);  } } else {  free_pidmap(pid);  pid = PTR_ERR(p); } return pid;}

子进程如何执行?

系统调用返回时会触发进程调度,这时候子进程会被优先执行(如果同cpu且不共享页表),子进程描述符thread字段的值被装入几个cpu寄存器,特别是esp(内核态堆栈指针)和eip(ret_from_fork()函数的地址),最后schedule_tail()函数用存放在栈中的值装载所有的寄存器,并强迫cpu返回到用户态。子进程最终得到执行。之后进程调度,父进程可能得到执行。这就是为什么fork()执行一次,返回两次。

3.Linux操作系统的进程状态转换

有以下进程状态:

彩世界开奖app官网 16

 

进程状态的转换:

彩世界开奖app官网 17

 

具体转换分析:

(1)进程的初始状态

进程是通过fork系列的系统调用(fork、clone、vfork)来创建的,内核(或内核模块)也可以通过kernel_thread函数创建内核进程。这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份,得到子进程。(可以通过选项参数来决定各种资源是共享、还是私有。)那么既然调用进程处于TASK_RUNNING状态(否则,它若不是正在运行,又怎么进行调用?),则子进程默认也处于TASK_RUNNING状态。另外,在系统调用调用clone和内核函数kernel_thread也接受CLONE_STOPPED选项,从而将子进程的初始状态置为 TASK_STOPPED。

 

(2)进程状态变迁

进程自创建以后,状态可能发生一系列的变化,直到进程退出。而尽管进程状态有好几种,但是进程状态的变迁却只有两个方向——从TASK_RUNNING状态变为非TASK_RUNNING状态、或者从非TASK_RUNNING状态变为TASK_RUNNING状态。也就是说,如果给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入 TASK_RUNNING状态),然后再响应SIGKILL信号而退出(变为TASK_DEAD状态)。并不会从TASK_INTERRUPTIBLE状态直接退出。进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中。于是被唤醒的进程将有机会被调度执行。

而进程从TASK_RUNNING状态变为非TASK_RUNNING状态,则有两种途径:

  • 响应信号而进入TASK_STOPED状态、或TASK_DEAD状态;
  • 执行系统调用主动进入TASK_INTERRUPTIBLE状态(如nanosleep系统调用)、或TASK_DEAD状态(如exit 系统调用);或由于执行系统调用需要的资源得不到满足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。

 

进程状态

进程描述符中的state域记录进程当前的状态,进程一共有五中状态,分别为:

  • TASK_RUNNING 运行
  • TASK_INTERRUPTIBLE 可中断
  • TASK_UNINTERRUPTIBLE 不可中断
  • __TASK_TRACED 被其他进程跟踪的进程
  • __TASK_STOPPED 进程停止执行

3.1 进程状态

task_struct中的state字段描述了进程当前所处的状态:

TASK_RUNNING:可运行状态,运行或等待运行。 TASK_ INTERRUPTIBLE:可中断的等待状态,进程被挂起,直到某个条件变为真。如硬件中断,等待的资源被释放,接受一个信号。 TASK_UNINTERRUPTIBLE:不可中断的等待状态,与可中断的等待状态类似,但不能被信号中断。仅用在特定情况(进程必须等待,直到一个不能被中断的事件发生),例如当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时,探测完成以前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。 TASK_ STOPPED:暂停状态,进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号后会进入暂停状态。 TASK_ TRACED:跟踪状态,进程的执行由debugger程序暂停,当一个进程被另一个进程监控时,任何信号都可以把这个进程置于TASK_TRACED状态。
EXIT_ZOMBLE:僵死状态,进程死亡,等待父进程获取其死亡信息。此时进程描述符还不能删除。 EXIT_DEAD:僵死撤消状态,进程彻底消亡,为防止竞争条件(其他进程再一次wait()),而设置EXIT_DEAD。

宏:

set_task_state宏:设置指定进程的状态。 set_current_state宏:设置当前进程的状态。

进程状态转换图:

彩世界开奖app官网 18

进程上下文

通常进程的代码在用户空间执行,当执行了系统调用或触发了某个异常时,它就陷入了内核空间。此时,我们称内核处于进程上下文中。

4、进程从何而来?

进程创建

  1. 写时拷贝,父子进程共享同一个地址空间,将页的拷贝推迟到实际发生写入时才进行。这个优化可以避免创建进程时拷贝大量不被使用的数据。
  2. 在进程中调用fork()会通过复制一个现有进程来创建一个新进程,调用fork()的进程是父进程,创建的进程是子进程。fork()函数从内核返回两次,一次是返回父进程,另一次返回子进程。Linux通过 clone(SIGCHLD, 0);系统调用实现fork()。
  3. vfork() 不拷贝父进程的页表项,其它与fork功能相同。系统实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
  4. exec()这组函数可以创建新的地址空间,并把新的程序载入其中。

1、概念

线程实现

在Linux内核中线程看起来就是一个普通的进程,只是和其它一些进程共享某些资源,如地址空间。

  1. 创建线程同样使用clone实现,只是需要传递一些参数标志来指明需要共享的资源:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
  2. 内核线程没有独立的地址空间,只在内核空间运行,不切换到用户空间上去,只能由内核线程创建。

2.2 进程链表

内核将所有进程用链表链接。链表头是init_task描述符,它是cpu0上的0进程,也叫swapper进程。 疑问: swapper是per cpu的,那链表头有多个?不是,只有cpu0的0进程描述符才是init_task这个静态全局变量,其它cpu的0进程描述符不是init_task,也不是静态,也不是全局。 几个操作宏:
SET_LINKS宏:从进程链表中插入一个task_struct; REMOVE_LINKS宏:从进程链表中删除一个task_struct;
for_each_process宏:从init_task开始遍历进程链表。

1.1 什么是进程?

进程是程序执行的一个实例,可以看作充分描述程序已经执行到何种程度的数据结构的汇集。 从内核观点看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体。 我们熟悉的fork()库函数,它有两种用法: (1)、一个父进程希望复制自己,使父子进程执行不同的代码段,常用于网络服务程序。 (2)、一个进程要执行一个不同的程序,fork()后立即exec(),如shell。

6、进程资源限制

每个进程都有一组相关的资源限制,避免用户过分使用系统资源(CPU,磁盘等)。 当前进程的限制存放在current->signal->rlim[]数组字段,数组每一项元素代表一种资源

这些资源包括:

// include/asm-generic/resource.h #define RLIMIT_CPU      0               /* CPU time in ms */#define RLIMIT_FSIZE    1               /* Maximum filesize */#define RLIMIT_DATA     2               /* max data size */#define RLIMIT_STACK    3               /* max stack size */#define RLIMIT_CORE     4               /* max core file size */#define RLIMIT_RSS      5               /* max resident set size */#define RLIMIT_NPROC    6               /* max number of processes */#define RLIMIT_NOFILE   7               /* max number of open files */#define RLIMIT_MEMLOCK  8               /* max locked-in-memory address space */#define RLIMIT_AS       9               /* address space limit */#define RLIMIT_LOCKS    10              /* maximum file locks held */#define RLIMIT_SIGPENDING 11            /* max number of pending signals */#define RLIMIT_MSGQUEUE 12              /* maximum bytes in POSIX mqueues */ #define RLIM_NLIMITS    13

其中rlim字段数据结构:

// include/linux/resource.h struct rlimit {        unsigned long   rlim_cur;        unsigned long   rlim_max;};

getrlimit()系统调用:读取rlim_cur
setrlimit()系统调用:改变rlim_cur,以rlim_max为上限。 只有具有CAP_SYS_ESOURCE权限的超级用户才能改变rlim_max。 大多数资源限制RLIMIT_INFINITY,即内核没有对资源限制。然而系统管理员可以给一些资源施加更强的限制。

1、概念 1.1 什么是进程? 进程是程序执行的一个实例,可以看作充分描述程序已经执行到何种程度的数据结构的汇...

3.2 各个状态的进程的组织

TASK_RUNNING:进程调度必须快速找出最佳可运行进程,因此可运行进程的组织结构至关重要。 Linux2.6为了让调度程序能在固定的时间内选出“最佳”可运行进程,建立了多个可运行进程链表,每种进程优先权(0~139)对应一个不同的链表。在SMP中,每个CPU有自己的进程链表集。 TASK_ INTERRUPTIBLE、TASK_UNINTERRUPTIBLE:进程被划分为多个子类,每个子类对应一个特定事件。这种情况下,进程状态没有提供足够的信息来快速恢复进程,所以有必要引进附加的进程链表。linux用等待队列实现这样的链表。 等待队列是Linux中实现的一种机制,它实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。 为了避免“惊群效应”,等待队列分为两种:资源互斥类非互斥类。等待互斥类资源的进程由内核有选择的唤醒,而非互斥进程总是有内核在事件发生时唤醒。 TASK_STOPPED、EXIT_ZOMBIE、EXIT_DEAD:没有专门的链表。

5、进程切换

这里只涉及内核如何完成进程切换,而不涉及调度机制和算法策略。也就是说,这里假定调度程序已经选好了合适的进程,如何换下旧进程,装上新进程。

尽管每个进程可以拥有属于自己的地址空间,但所有的进程必须共享cpu寄存器。因此cpu寄存器的保存和恢复是进程切换的重要内容。 进程恢复前必须装入的一组数据称为硬件上下文,Linux中,硬件上下文一部分存放在task_struct,剩余部分存放在内核态堆栈中。
进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。 task_struct中的类型为thread_struct的thread字段用于在进程切换时保存硬件上下文,它包含了大部分CPU寄存器,但不包括eax等通用寄存器,通用寄存器保留在内核堆栈中。

进程切换由两步组成:

1、切换页全局目录以安装一个新的地址空间。 2、切换内核态堆栈和硬件上下文。由switch_to宏完成。

4.1 进程间关系

由于进程不是凭空创见,而是由已有进程复制创建的,所以进程之间有父子关系。也就是说,Linux进程之间的关系可以组织为一棵树,其根节点为0号进程。

task_struct中相关字段:

real_parent:创建pid为p的进程为父进程,如果这个父进程不复存在(如父进程先于子进程死亡),进程p就由init进程(1号进程)收养。 parent:通常与real_parent一致,但偶尔不同,如另一个进程发出监控p的ptrace()系统调用请求时。 children:子进程链表头。 sibling:兄弟进程链表。

除了父子关系,进程还存在其他关系(线程组,进程组,登录会话,调试跟踪):

group_leader:当前进程p所在进程组的领头进程的描述符指针。 signal->pgrp:p所在进程组的领头进程的PID。 tgid:p所在线程组的领头进程的PID,getpid()系统调用返回该值,而不是pid。 signal->session:p的登录会话领头进程的PID。 ptrace_children:被debugger程序跟踪的p的子进程的链表头。 ptrace_list:指向所跟踪进程其实际父进程链表的前一个和下一个元素。

进程组和会话中的进程安排:

进程组通常是由shell的管道线将几个进程编成一组的。例如,由下列形式的shell命令形成的进程组会话如下图:

proc1 | proc2 &proc3 | proc4 | proc5

彩世界开奖app官网 19
因此,一个进程拥有4个id:PID,tgid,pgrp,session。内核为了加速查找,以这四个值为索引,将task_struct组织为4个散列表,并用链表来解决散列冲突。
task_struct结构中:struct pid pids[4];

struct pid{    int nr;//冗余?    struct hlist_node pid_chain;    struct list_head pid_list;}

所以散列表看起来是这样的:
彩世界开奖app官网 20

3、进程状态及各状态的组织

4.3 进程到底从何而来——从start_kernel()开始

//  init/main.c/* * Activate the first processor. */ asmlinkage void __init start_kernel(void){ char * command_line; extern struct kernel_param __start___param[], __stop___param[];/* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); page_address_init(); printk(linux_banner); setup_arch(&command_line); setup_per_cpu_areas(); /*  * Mark the boot cpu "online" so that it can call console drivers in  * printk() and can access its per-cpu storage.  */ smp_prepare_boot_cpu();  /*  * Set up the scheduler prior starting any interrupts (such as the  * timer interrupt). Full topology setup happens at smp_init()  * time - but meanwhile we still have a functioning scheduler.  */ sched_init(); /*  * Disable preemption - early bootup scheduling is extremely  * fragile until we cpu_idle() for the first time.  */ preempt_disable(); build_all_zonelists(); page_alloc_init();//初始化伙伴系统 printk("Kernel command line: %sn", saved_command_line); parse_early_param(); parse_args("Booting kernel", command_line, __start___param,     __stop___param - __start___param,     &unknown_bootoption); sort_main_extable(); trap_init(); rcu_init(); init_IRQ(); pidhash_init(); init_timers(); softirq_init(); time_init(); /*  * HACK ALERT! This is early. We're enabling the console before  * we've done PCI setups etc, and console_init() must be aware of  * this. But we do want output early, in case something goes wrong.  */ console_init(); if (panic_later)  panic(panic_later, panic_param); profile_init(); local_irq_enable();#ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok &&   initrd_start < min_low_pfn << PAGE_SHIFT) {  printk(KERN_CRIT "initrd overwritten (0xlx < 0xlx) - "      "disabling it.n",initrd_start,min_low_pfn << PAGE_SHIFT);  initrd_start = 0; }#endif vfs_caches_init_early(); mem_init(); kmem_cache_init();//初始化slab numa_policy_init(); if (late_time_init)  late_time_init(); calibrate_delay();//确定cpu时钟速度 pidmap_init(); pgtable_cache_init(); prio_tree_init(); anon_vma_init();#ifdef CONFIG_X86 if (efi_enabled)  efi_enter_virtual_mode();#endif fork_init(num_physpages); proc_caches_init(); buffer_init(); unnamed_dev_init(); security_init(); vfs_caches_init(num_physpages); radix_tree_init(); signals_init(); /* rootfs populating might need page-writeback */ page_writeback_init();#ifdef CONFIG_PROC_FS proc_root_init();#endif check_bugs(); acpi_early_init(); /* before LAPIC and SMP init */ /* Do the rest non-__init'ed, we're now alive */ rest_init();//继续,后面会创建1号init进程,最后cpu_idle(),用以cpu没进程执行时替补}/* * We need to finalize in a non-__init function or else race conditions * between the root thread and the init thread may cause start_kernel to * be reaped by free_initmem before the root thread has proceeded to * cpu_idle. * * gcc-3.4 accidentally inlines this function, so use noinline. */ static void noinline rest_init(void) __releases(kernel_lock){ kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); numa_default_policy(); unlock_kernel(); preempt_enable_no_resched(); cpu_idle();} 

疑问: 并不是只有代码就可以执行,没有内核栈,task_struct等数据结构怎么行?
0号进程使用的所有数据结构都是静态创建的(所有其他进程的数据结构都是动态分配的),也就是说,当磁盘中的内核映像加载如内存的时候,0号进程的数据结构也就有了。 0号进程完成诸多初始化的光荣使命之后,循环在cpu_idle(),这时只有在cpu没有可调度的进程时,就会执行0号进程。 SMP系统中,每个CPU都有一个进程0.启动时只用到一个cpu,禁用其他CPU,当0进程激活其他CPU时,通过copy_process()创建其他cpu的0号进程。 【linux-2.6.11】init() --> smp_prepare_cpus() --> smp_boot_cpus() --> do_boot_cpu() --> idle = fork_idle() --> task = copy_process()
进程1 进程0使用kernel_thread()创建进程1,此时的进程1还是内核线程,它执行内核中init( )函数[init/main.c],继续初始化工作。 之后init()调用execve()装入可执行程序,一般是/sbin/init。 这个程序的主要工作是:用户系统初始化,启动各种deamon进程(根据《UNIX环境高级编程》,内核线程也属于守护进程),启动tty和图形界面。
此时init内核线程变为一个普通进程。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。

static int init(void * unused){ lock_kernel(); /*  * Tell the world that we're going to be the grim  * reaper of innocent orphaned children.  *  * We don't want people to have to make incorrect  * assumptions about where in the task array this  * can be found.  */ child_reaper = current; /* Sets up cpus_possible() */ smp_prepare_cpus(max_cpus);   /* 这里创建其他0号进程 */ do_pre_smp_initcalls(); fixup_cpu_present_map(); smp_init(); sched_init_smp(); /*  * Do this before initcalls, because some drivers want to access  * firmware files.  */ populate_rootfs(); do_basic_setup(); /*  * check if there is an early userspace init.  If yes, let it do all  * the work  */ if (sys_access((const char __user *) "/init", 0) == 0)  execute_command = "/init"; else  prepare_namespace(); /*  * Ok, we have completed the initial bootup, and  * we're essentially up and running. Get rid of the  * initmem segments and start the user-mode stuff..  */ free_initmem(); unlock_kernel(); system_state = SYSTEM_RUNNING; numa_default_policy(); if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)  printk("Warning: unable to open an initial console.n"); (void) sys_dup(0); (void) sys_dup(0);  /*  * We try each of these until one succeeds.  *  * The Bourne shell can be used instead of init if we are   * trying to recover a really broken machine.  */ if (execute_command)  run_init_process(execute_command); run_init_process("/sbin/init"); run_init_process("/etc/init"); run_init_process("/bin/init"); run_init_process("/bin/sh"); panic("No init found.  Try passing init= option to kernel.");}

其他内核线程 内核中有一些重要任务,以现行的方式执行效率不高,委托给独立的调度执行流做比较合适。每个进程就是一个独立的执行流,而这些为内核工作的执行流共享使用内核的地址空间等资源,于是就被叫做内核线程。 Linux使用很多其他内核线程,其中一些在初始化阶段创建,一些在系统运行过程中动态创建。

一些内核线程的例子:


keventd(也被称为事件):执行keventd_wq工作队列中的函数。 kapmd:处理与高级电源管理(APM)相关的事件。 kswapd:执行周期内存回收。 pdflush:刷新“脏”缓冲区中的的内容到磁盘以回收内存。 kblockd:执行kblockd_workqueue工作队列中的函数,周期性的激活块设备驱动程序。 ksoftirqd:运行tasklet,系统中每cpu都有这样一个内核线程。


1.3 Linux内核如何实现线程?

Linux内核并没有标准的线程,linux使用轻量级进程的方式实现线程,也可以认为轻量级进程就是Linux线程。 所谓轻量级进程,就是它的资源并不是独享的,而是与一组轻量级进程共享。这就是线程组。 getpid()、kill()、_exit()这样的一些系统调用,对线程组整体起作用。 仅仅这些内核这些功能还远远不够,必须有用户线程库的支持。线程库使用户看起来,线程和进程是独立的概念。 POSIX兼容的pthread库:LinuxThreads,Native Posix Thread Library(NPTL)、IBM的Next Generation Posix Threading Package(NGPT) 线程使用独立于进程的一套库,pthread_create(...),pthread_exit(...),pthread_join(...), pthread_cancel(...)

Linux内核剖析之进程简介

2、预备知识:进程描述符task_struct

内核将关于一个进程的所有信息放在一个结构体里以方便管理。 严格的一一对应,进程,轻量级进程,内核线程。 想一想,一个进程会有哪些信息?【include/linux/sched.h: struct task_struct】 彩世界开奖app官网 21
进程标识符PID:使用唯一的数字来标识当前进程,task_struct的pid字段用来存放pid,进程按创建先后被顺序编号,pid值达到上限就回滚使用闲置的小pid,内核管理一个pid位图pidmap_array。 另外注意,当考虑到线程这个因素之后,pid含义不再是进程id,而只能看成是进程线程全局唯一的数字标识。应用经常需要的是进程id,所以引入tgid字段,对进程它是自己的pid,对轻量级进程它是领头进程pid,getpid()系统调用返回的是tgid值而不是pid值。 task_struct结构体的声明如下:

struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ struct thread_info *thread_info; atomic_t usage; unsigned long flags; /* per process flags, defined below */ unsigned long ptrace;  int lock_depth;  /* Lock depth */  int prio, static_prio; struct list_head run_list; prio_array_t *array;  unsigned long sleep_avg; unsigned long long timestamp, last_ran; int activated;  unsigned long policy; cpumask_t cpus_allowed; unsigned int time_slice, first_time_slice; #ifdef CONFIG_SCHEDSTATS struct sched_info sched_info;#endif  struct list_head tasks; /*  * ptrace_list/ptrace_children forms the list of my children  * that were stolen by a ptracer.  */ struct list_head ptrace_children; struct list_head ptrace_list;  struct mm_struct *mm, *active_mm; /* task state */ struct linux_binfmt *binfmt; long exit_state; int exit_code, exit_signal; int pdeath_signal;  /*  The signal sent when the parent dies  */ /* ??? */ unsigned long personality; unsigned did_exec:1; pid_t pid; pid_t tgid; /*   * pointers to (original) parent process, youngest child, younger sibling,  * older sibling, respectively.  (p->father can be replaced with   * p->parent->pid)  */ struct task_struct *real_parent; /* real parent process (when being debugged) */ struct task_struct *parent; /* parent process */ /*  * children/sibling forms the list of my children plus the  * tasks I'm ptracing.  */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */ struct task_struct *group_leader; /* threadgroup leader */  /* PID/PID hash table linkage. */ struct pid pids[PIDTYPE_MAX];  struct completion *vfork_done;  /* for vfork() */ int __user *set_child_tid;  /* CLONE_CHILD_SETTID */ int __user *clear_child_tid;  /* CLONE_CHILD_CLEARTID */  unsigned long rt_priority; unsigned long it_real_value, it_real_incr; cputime_t it_virt_value, it_virt_incr; cputime_t it_prof_value, it_prof_incr; struct timer_list real_timer; cputime_t utime, stime; unsigned long nvcsw, nivcsw; /* context switch counts */ struct timespec start_time;/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt;/* process credentials */ uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; struct group_info *group_info; kernel_cap_t   cap_effective, cap_inheritable, cap_permitted; unsigned keep_capabilities:1; struct user_struct *user;#ifdef CONFIG_KEYS struct key *session_keyring; /* keyring inherited over fork */ struct key *process_keyring; /* keyring private to this process (CLONE_THREAD) */ struct key *thread_keyring; /* keyring private to this thread */#endif int oomkilladj; /* OOM kill score adjustment (bit shift). */ char comm[TASK_COMM_LEN];/* file system info */ int link_count, total_link_count;/* ipc stuff */ struct sysv_sem sysvsem;/* CPU-specific state of this task */ struct thread_struct thread;/* filesystem information */ struct fs_struct *fs;/* open file information */ struct files_struct *files;/* namespace */ struct namespace *namespace;/* signal handlers */ struct signal_struct *signal; struct sighand_struct *sighand;  sigset_t blocked, real_blocked; struct sigpending pending;  unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask;  void *security; struct audit_context *audit_context; /* Thread group tracking */    u32 parent_exec_id;    u32 self_exec_id;/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */ spinlock_t alloc_lock;/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */ spinlock_t proc_lock;/* context-switch lock */ spinlock_t switch_lock; /* journalling filesystem info */ void *journal_info; /* VM state */ struct reclaim_state *reclaim_state;  struct dentry *proc_dentry; struct backing_dev_info *backing_dev_info;  struct io_context *io_context;  unsigned long ptrace_message; siginfo_t *last_siginfo; /* For ptrace use.  *//* * current io wait handle: wait queue entry to use for io waits * If this thread is processing aio, this points at the waitqueue * inside the currently handled kiocb. It may be NULL (i.e. default * to a stack based synchronous wait) if its doing sync IO. */ wait_queue_t *io_wait;/* i/o counters(bytes read/written, #syscalls */ u64 rchar, wchar, syscr, syscw;#if defined(CONFIG_BSD_PROCESS_ACCT) u64 acct_rss_mem1; /* accumulated rss usage */ u64 acct_vm_mem1; /* accumulated virtual memory usage */ clock_t acct_stimexpd; /* clock_t-converted stime since last update */#endif#ifdef CONFIG_NUMA   struct mempolicy *mempolicy; short il_next;#endif};

本文由彩世界开奖app官网发布于彩世界开奖app官网,转载请注明出处:Linux内核学习笔记——进程管理彩世界开奖app官网

关键词: Android日积月累