深入理解linux内核笔记-第三章进程process

进程是什么

From the kernel’s point of view, the purpose of a process is to act as an entity of which system resources (CPU time, memory, etc) are allocated.
进程其实就是一个系统资源分配的集合实体

Although the parent and child may share the pages containing the program code, they have separate copies of data (stack and heap), so that changes by the child to a memory location are invisible to parent (and vice versa)
父子进程对于代码部分是相同的映射,但是它们有各自分开独立的数据部分(堆和栈),因此各自对自己数据部分的修改对方是不可见的

They support multithreaded applications—user programs having many relatively independent execution flows sharing a large portion of the application data structures. In such systems, a process is composed of several user threads, each of which represents an execution flow of the process. Nowadays, most multithread application are written using standard set of library functions called pthread (POSIX thread) libraries.
现代Unix系统支持多线程程序,进程由多个用户线程组成,这些线程共享进程的数据结构;如今的大多数多线程程序依据pthread库的标准库函数集编写

From the kernel point of view, a multithread application was just a normal process. The multiple execution flows of a multithread application were created, handled, and scheduled entirely in User Mode.
在内核看来,一个多线程应用程序只是一个普通进程。多线程应用程序的多个执行线程的创建,处理和调度都是在用户态下进行的;

Linux uses lightweight process to offer better support for multithread applications. Basically, two lightweight processes may share some resources, like the address space, the open files, and so on.
似乎在linux中,轻量级进程就是所谓的线程,线程间共享诸如地址空间,文件等资源,这意味着相对于进程间通信,线程通信开销非常小,因为它们的资源是共享可见的;而且线程的创建相对与进程也很小;

This is the role of the process descriptor—a task_struct type structure whose fields contain all the information related to a single process. In addition to a large number of fields containing process attribute, the process descriptor contains several pointer to the other data structures.
进程描述符都是task_struct这种结构体的结构,包含了单个进程相关的所有信息。这些信息不光是些进程的属性,还要很多指向其他数据结构的指针信息

进程的状态

就绪状态和运行状态
就绪状态的状态标志state的值为TASK_RUNNING。此时,程序已被挂入运行队列,处于准备运行状态。一旦获得处理器使用权,即可进入运行状态。

当进程获得处理器而运行时 ,state的值仍然为TASK_RUNNING,并不发生改变;但Linux会把一个专门用来指向当前运行任务的指针current指向它,以表示它是一个正在运行的进程。

可中断等待状态
状态标志state的值为TASK_INTERRUPTIBL。此时,由于进程未获得它所申请的资源而处在等待状态。一旦资源有效或者有唤醒信号,进程会立即结束等待而进入就绪状态。

不可中断等待状态
状态标志state的值为TASK_UNINTERRUPTIBL。此时,进程也处于等待资源状态。一旦资源有效,进程会立即进入就绪状态。这个等待状态与可中断等待状态的区别在于:处于TASK_UNINTERRUPTIBL状态的进程不能被信号量或者中断所唤醒,只有当它申请的资源有效时才能被唤醒。

这个状态被应用在内核中某些场景中,比如当进程需要对磁盘进行读写,而此刻正在DMA中进行着数据到内存的拷贝,如果这时进程休眠被打断(比如强制退出信号)那么很可能会出现问题,所以这时进程就会处于不可被打断的状态下。

停止状态
状态标志state的值为TASK_STOPPED。当进程收到一个SIGSTOP信号后,就由运行状态进入停止状态,当受到一个SIGCONT信号时,又会恢复运行状态。这种状态主要用于程序的调试,又被叫做“暂停状态”、“挂起状态”。

中止状态
状态标志state的值为TASK_DEAD。进程因某种原因而中止运行,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外,并且系统对它不再予以理睬,所以这种状态也叫做“僵死状态”,进程成为僵尸进程。
在这里插入图片描述

PID

The PID of a newly created process is normally the PID of the previously created process increased by one.
进程的PID一般都是上一个创建的进程的PID+1

The kernel must manage a pidmap_array bitmap that denotes which are the PIDs currently assigned and which are the free ones.
内核使用bitmap_array来标识哪个PID已被分配了,哪个PID是可用的

Unix programmers expect threads in the same group to have a common PID.
Unix中一个线程组中的所有线程有着同样的PID

怎么找到进程描述符的位置?

fork函数在创建进程时,会为进程的内核栈和task_struct即进程描述符分配空间(进程的内核栈在内核数据段中);那么内核如何快速有效的找到当前运行进程的进程描述符所在的位置呢?Linux的方式是通过内核栈指针esp;
那么怎么找到esp的位置呢?是通过对应进程的TSS中保存的esp信息得到的,通过TR寄存器可以找到当前进程的TSS;
We learn that a process in Kernel Mode accesses a stack contained in the kernel data segment, which is different from the stack used by the process in User Mode.
进程在系统调用陷入内核态时,会需要使用进程的内核栈(这里的内核栈,是当中断发生在用户态时,将此时cs,eip,eflag,ss,esp保存在这个进程内核栈中;详见杨师兄提供的第四章第16页),而内核栈是位于内核数据段中的;而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈;(这里的内核栈,指的是进程的内核栈)

在这里插入图片描述
在动态线性地址(用户)空间中,存放thread_union,大小为8K,内核栈和thread_info结构体紧凑的放在这个8K的连续页中;因为这个8K空间的地址是2^13的倍数,因此可以很简单的将esp的低13位屏蔽掉,得到的就是thread_info结构的指针;而task字段在thread_info中的偏移为0,因此取esp屏蔽掉低13位后指针的值就是task_struct的地址;
总结:esp->thread_info->current_task_struct
Earlier versions of Linux did not store the kernel stack and the process descriptor together. Instead, they were forced to introduce a global static variable called ‘current’ to identify the process descriptor of the running process. One multiprocessor systems, it was necessary to define current as an array–one element for each available CPU
早期的Linux并没有采用上面将内核栈和thread-info放在一起的方式,而是提出了‘current’变量来专门的指向当前进程的PCB。对于多核系统,这个‘current’就是个列表,每一项对于于一个CPU当前运行的进程PCB

进程描述符链表

For each list, a set of primitive operations must be implemented: initializing the list, inserting and deleting an element, scanning the list, and so on.
The first example of a doubly linked list we will examine is the process list, a list that links together all existing process descriptor.
Linux中采用链表将所有的存在进程的描述符链接在一起;每个进程描述符task_struct结构体中都有一个list_head类型的字段,这个字段由next,prev两个指针组成,指向下一个和前一个进程的list_head字段;排在最前面的进程叫init_task,其list_head字段中的prev指向进程链表的最后一项;

在这里插入图片描述

就绪态进程链表

Earlier Linux versions put all runnable processes in the same list called runqueue. Because it would be too costly to maintain the list ordered according to process priorities, the earlier schedules were compelled to scan the whole list in order to select the “best” runnable process.
早期的linux通过建立一个叫做runqueue的链表来记录链接所有可以运行的进程,这里可以运行指的是进程的状态(TASK_RUNNING state,书本的话来说就是就绪态);为了减少以优先级为顺序维护这个runqueque的开销(空间开销),早期的进程调度器通过遍历整个runqueue的方法来找出目前最适合那个就绪态进程;很显然,随着进程的增多,这种遍历开销会越来越大(时间开销);

The trick used to achieve the scheduler speedup consists of splitting the runqueue in many lists of runnable processes, one list per process priority. Each task_struct descriptor includes a run_list field of type list_head.
在Linux2.6中,将早期的runqueue分割为多个‘可运行’进程链表;进程的优先级为0-139,在每个进程描述符中包括一个list_head类型的run_list字段;
所有这些被分割的链表由prio_array_t这种类型的结构管理,其中有:

  1. nr_active:用于记录链表中进程描述符的数目
  2. bitmap,unsigned long[5],是一个58Byte4bit=160bit的优先级bitmap(因为有140个优先级)
  3. queue,struct list_head[140],是140个优先级的链表的表头
    一个CPU的核心对应一个运行队列。每个进程在同一时刻只能处于一个运行队列里。
    初始化时,bitmap中的每一位都置位0,当某进程变可运行的时候(也就是说它的状态变成TASK_RUNNING),其优先级对应于bitmap中的位置1。这样就简化了搜寻的工作量——要找到当前可运行的最高优先级的进程,只需要找到bitmap中第一个为1的位。因为优先级数目是固定的,所以搜寻工作的时间复杂度不会受到当前进程数目的影响。
    Process in a TASK_INTERRUPTIBLE or TASJ_UNITERRUPTIBLE state are subdivided into many classes, each of which corresponds to a specific event.
    对于zombie或orphans状态的进程并不需要像TASK_RUNNING状态的进程一样为其构建专门的runnqueue列表,对它们的访问一般都是直接通过PID或是通过其父进程;

如何通过PID获得进程描述符位置

要想通过PID就获得进程描述符的位置,Linux引入了pidhash表,利用链表了处理冲突的PID,看图很容易理解:
在这里插入图片描述
在这里插入图片描述

阻塞态进程的等待队列

就绪态的进程有runqueue,被阻塞处于TASK_INTERRUPTIBLE or TASK_UNITERRUPTIBLE状态的进程,则根据其等待的事件划分为多个wait queue,等待队列
Each wait queue is identified by a wait queue head, a data structure of type wait_queue_head_t:

struct  __wait_que_head_t{
spinlock_lock;
struct list_head task_list;
}

每个等待队列都是通过一个等待队列头结点来标识的;通过其数据结构可知,等待队列的同步是由spinlock自旋锁类型的lock来实现的,task_list是等待进程队列的表头,同前面一样是list_head双向链表结构;

Elements of a wait queue list are of type wait_queue_t:

struct __wait_queue {
unsigned int flags; //flags=0为非互斥,flags=1为互斥
struct task_struct *task; //指向进程描述符的指针
wait_queue_func_t func; //等待进程的唤醒方式
struct list_head task_list; //等待同一事件的等待进程都用list_head链接在一起
}; //一个等待队列元素

这个等待进程队列的元素结构如上,每个元素代表一个睡眠进程;flags用于标志是互斥进程(flag=1,‘exclusive process’)还是非互斥进程(flag=0,’nonexclusive process‘),

A process waiting for a resource the can be granted to just one process at a time is a typical exclusive process. Process waiting for an event that may concern any of them are nonexclusive.
等待临界资源的是互斥进程,等待相关事件完成的是非互斥进程,就像一个传输事件的完成将唤醒所有等待这个事件的进程;

若是互斥进程则当事件被满足时由内核有选择地将一个进程从等待队列唤醒,反之是非互斥进程则当事件被满足时内核将所有等待这个资源的进程都唤醒;task是一个指向其进程描述符的指针;func是决定这个等待进程的唤醒方式;

对于如何将进程插入等待队列,linux提供了很多函数,如:
init_waitqueue_entry(q,p),初始化了一个等待队列中的等待项(设置flag,task,func)

The add_wait_queue() function inserts a nonexclusive process in the first position of a wait queue list. The add_wait_queue_exclusive() function inserts an exclusive process in the last position of a wait queue list.
add_wait_queue()函数将非互斥进程插入等待队列的首位,将互斥进程插入等待队列的末尾

sleep_on(),但是它不能被用在竞争条件??
sleep_on_timeout(),让进程等待一定的时间,到时就唤醒
wait_event(),wait_event_interruptible是marco宏,用于使调用进程在等待队列中等待,直到修改了给定条件为止;

还有用于唤醒进程的一些宏,将它们的状态设置为TASK_RUNNING:
wake_up,wake_up_nr,wake_up_all,wake_up_interruptible…
以wake_up()为例,它将从头遍历整个等待队列,唤醒所有进程,直到遇到一个互斥进程(也唤醒这一个互斥进程);为什么是这样的呢?因为当一个条件被满足时,所有的等待这个条件的非互斥进程都会被唤醒,而互斥资源则由内核有选择地挑一个;
书中的注释还提了一句,一个等待进程队列里同时有互斥资源和非互斥资源是非常罕见的,看来通常情况一个等待队列要么都是互斥要么都不是互斥

进程的资源限制

Each process has an associated set of ‘resource limits’, which specify the amount of system resources it can use.
The resource limits for the current process are stored in the ‘current->signal->rlim’ field, that is, in a field of the process’s signal descriptor. The field is an array of elements of type struct ‘rlimit’, one of each resource limit:

struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};

每个进程都有其对应的资源限制,当前进程的资源限制保存在current->signal->rlim字段,这个current是前面提到过的指向当前进程描述符的指针,signal是进程的信号描述符(到11章才介绍)。这个rlim字段的类型为rlimit结构体,有rlim_cur是标识当前的资源上限,rlim_max 是标识最高可以达到的上限。

By using the ‘getrlimit() and setrlimit()’ system calls, a user can always increase the ‘rlim cur’ limit of some resource up to ‘rlim_max’
使用getrlimit或setrlimit这两个系统调用可以将当前进程的当前限制提到最大上限rlim_max;

大多数的资源限制包含RLIM_INFINITY (0xffffffff),也就是不设上限,当然实际还是要依据RAM大小,磁盘剩余等;但是系统管理员可以给进程施加更强制的资源上限,当一个用户试图login in的时候,kernel就创建一个superuser的进程,该特权进程可以通过调用setrlimit()来降低rlim_cur或rlim_max,随后经过login in这个进程归用户所有,而用户之后执行的进程都是这个进程的子进程,都继承了父进程的资源限制;

进程切换

Intel 80x86提供了TSS来保存进程的硬件上下文,通过一个长跳指令:ljmp $TSS_SELECTOR就可以保存并切换硬件上下文;
但是Linux并不是使用这个intel提供的硬件支持的;Linux使用软件的做法,用Mov指令一步一步的实现,理由:
Step-by-step switching performed through a sequence of ‘mov’ instructions allows better control over the validity of the data being load. In particular, it is possible to check the values of the ‘ds’ and ‘es’ segmentation registers, which might be forged by malicious user. This type of checking is not possible when using a single far jump instruction.
The amount of time required by the old approach and the new approach is about the same. However, it’s not possible to optimize a hardware context switch, while might be have room for improving the current switching code.
一是因为软件实现更安全,可以检查ds和es是否被恶意写入非法值,这在硬件上是不可能做到的;
二是因为两种方法的时间基本相同,反而软件方法可能还有优化提升的空间;

Although Linux doesn’t use hardware context switches, it is nonetheless forced to set up a TSS for each distinct CPU in the system.
虽然并不使用TSS硬件方法的上下文切换, Linux还是为每个CPU都强制建立了一个TSS(注意,一个CPU一个TSS),原因如下:

  1. 当80x86的CPU要从用户态切换到内核态,会去TSS中取得对应的内核栈ESP;这和杨师兄提供的第四章中,去每个进程独有的TSS中取得对应的任务内核栈很相似;
  2. TSS保存有I/O port的bitmap,做IN或OUT指令的时候需要去检查
    每当进程切换的时候都会去更新TSS中的一部分(那一部分??是ESP部分吗??)

既然并不使用Intel提供的TSS机制,那么Linux中进程的硬件上下文保存在哪里呢??
Thus, each process descriptor included a field called ‘thread’ of type ‘thread_struct’, in which the kernel saves the hardware context whenever the process is being switched out.
This data structure includes fields for most of the CPU registers, except the general-purpose registers such as ‘eax’,’ebx’…, which are stored in the Kernel Mode stack.
Linux的大部分硬件上下文保存在每个进程描述符中的thread字段中,通用寄存器eax,ebx,ecx,edx等保存在内核栈中(是否是每个的进程的任务内核栈??)

执行进程的切换
进程切换可以分为两步:

  1. 切换到新的内存映射(页结构)
  2. 切换到新的内核栈(esp)和硬件上下文(eflags等)
    关于switch_to宏
    上面的第二步其实就是通过switch_to宏来实现的,在linux源码中是用gcc 汇编inline的形式编写的,gcc inline原理见csdn博客;
    难点在于switch_to使用的三个参数pre,next,last;
    进程的切换实际涉及三个进程,假设为A,B,C
    prev是要被切换的进程A描述符的地址,next是要切换到的下一个进程B描述符的地址,last表示宏把进程C的描述符地址写到了什么位置,实际上给last赋的就是prev,所以意思就是把进程C的描述符地址写入到prev处;
    这里为什么会提到进程C呢??按理说,只需要AB的信息就可以将进程A切换到进程B,再从B到其他进程,一直切换下去,再从进程C切换回到进程A(恢复进程A),但是这样会丢失掉对于进程C的reference关联,因为回到进程A,其内核栈中只有prev=A,next=B的信息;而书中说这个关联对于完成进程切换是非常有用的(具体有啥用要看到第七章进程调度);
    因此就引入了这个last参数,它可以在进程C切换到进程A的时候将eax中的value(就是prev即C的进程描述符地址)写入到last所在地址(prev),因此last参数作用之前回到进程A的内核栈,prev=A,next=B,last=A,last作用后将eax=C(因为进程C执行switch-to的时候eax=C)写入到last=prev=A,因此这时prev=C,next=B,last=A;然后继续切换的循环

在switch_to宏里面还调用了__switch_to() C函数,switch_to宏主要完成了prev进程的flag,esp入栈保护,并切换到next进程的内核栈(其实也就切换了current进程);调用的__switch_to()函数做的工作是去切换硬件上下文:

  1. 执行__unlazy_fpu()宏,保存prev进程的FPU,MMX,XMM
  2. 执行smp_processor_id()来从当前进程的thread_info中获取当前进程执行所在的cpu下标
  3. 将当前的esp保存到本地CPU的TSS的esp0字段中
  4. 把next进程的TLS段装入CPU的GDT,反正是用于多线程的
    TLS是Thread-Local Storage,线程本地存储,有一段解释很好:
    线程的同步是进行多线程编程要考虑的问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。若能够为每个线程提供一份需要共享的数据的copy,那么对该数据的访问也就没有必要进行同步了。TLS就是能够达到这个目的的一个多线程设计模式,指每个线程都拥有各自独立的数据拷贝。
  5. 把当前的fs,gs段寄存器保存在prev进程描述符的fs,gs字段中
  6. 不太理解
  7. 当next进程被挂起时正在使用调试寄存器,就将next进程描述符中的debugreg数组加载到dr0。。。dr7中
  8. 当next进程或pre进程使用的是其自己定制的I/O权限bitmap时,要更新TSS中的I/O位

保存和加载FPU,MMX和XMM寄存器

FPU是算术浮点单元(里面应该是些浮点寄存器)集成到CPU中,浮点算术函数是用ESCAPE指令来执行的,若一个进程在使用ESCAPE指令,那么浮点寄存器的内容也属于这个进程的硬件上下文,需要被保存;
在Pentium系列中,提出MMX指令,用于加速多媒体应用程序的执行,MMX指令作用于FPU的浮点寄存器;

MMX指令之所以可以加速多媒体应用程序的执行,是因为它引入了SIMD(single-instruction multiple-data)单指令多数据流水线;
Pentium 3 为SIMD提出了扩展,Streaming SIMD Extension,即SSE;
SSE为处理包含在8个128位寄存器(XMM寄存器)的浮点值增加功能;
Pentium 4又提出了SSE2;SSE和SSE2都是使用同一XMM寄存器集;

80x86并不会自动保存浮点寄存器(FPU,MMX,XMM),是通过CR0的TS标志位设置切换机制,详见控制寄存器文章;
注意进行浮点运算的指令就是ESCAPE和MMX,SSE,SSE2等;
这些浮点寄存器的内容是保存在进程的进程描述符中的,放在进程描述符中的thread.i387字段中,结构体为:
union i387_union {
struct i387_fsave_struct fsave;//供具有数学协处理器和MMX单元的CPU保存浮点寄存器
struct i387_fxsave_struct fxsave;//供具有SSE和SSE2扩展功能的CPU保存浮点寄存器
struct i387_soft_struct soft;//供无数学协处理器的CPU使用(无实际的,就会通过软件模拟

进程和线程的创建

clone(),fork()和vfork()
轻量级进程(linux中的线程)由clone来创建,和fork()和vfork()不同的是,clone()对进程创建的步骤控制更准确;

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

在书中还有:
tls,ptid,ctid这几个参数,似乎用的比较少

这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是flags可以取的值:
标志 含义

 CLONE_PARENT  创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
 CLONE_FS          子进程与父进程共享相同的文件系统,包括root、当前目录、umask
 CLONE_FILES     子进程与父进程共享相同的文件描述符(file descriptor)表
 CLONE_NEWNS  在新的namespace启动子进程,namespace描述了进程的文件hierarchy
 CLONE_SIGHAND  子进程与父进程共享相同的信号处理(signal handler)表
 CLONE_PTRACE  若父进程被trace,子进程也被trace
 CLONE_VFORK    父进程被挂起,直至子进程释放虚拟内存资源
 CLONE_VM          子进程与父进程运行于相同的内存空间
 CLONE_PID         子进程在创建时PID与父进程一致
 CLONE_THREAD   Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

clone() is actually a wrapper function defined in the C library, which sets up the stack of the new lightweight process and invokes a clone() system call hidden to the programmer.
clone()其实是POSIX的一个C库封装函数,它为新线程(这里把LWP直接叫线程吧)创建了堆栈,然后调用了clone()系统调用(这里要区别于前面的封装函数,同名但不一样);

The traditional fork() system call is implemented by Linux as a clone() system call whose ‘flags’ parameter specifies both a SIGCHLD signal and all the ‘clone’ flags cleared, and whose ‘child_stack’ parameter is the ‘current’ parent stack pointer.
传统的fork()系统调用其实就是基于clone()系统调用来实现的,其中clone()的flags参数为SIGCHLD,这就使得fork()出来的子进程和父进程共享用户态堆栈;

The vfork() system call, is implemented by Linux as a clone() system call whose flags parameter specifies both a SIGCHLD signal and the flags CLONE_VM and CLONE_VFORK, and whose ‘child_stack’ parameter is equal to the current parent stack pointer.
vfork()系统调用在Linux中也是使用clone()系统调用来实现的,这里的clone()的flags参数为SIGCHLD,CLONE_VM,CLONE_VFORK,相当于父子共享内存描述符(??)和页表,并且子进程也和父进程共享用户态堆栈;

这里总结下clone,fork,vfork的区别:
他们都是基于clone()系统调用实现的;
1.fork是最简单的调用,不需要任何参数,仅仅是在创建一个子进程并为其创建一个独立于父进程的空间。fork使用COW(写时拷贝)机制,子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段。也就是说,对于程序中的数据,子进程要复制一份,但是对于指令,子进程并不复制而是和父进程共享。
2.vfork是一个过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间(一切空间)。在vfork创建子进程之后,父进程阻塞(因为一切共享,所以父子不可能同时进行),直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。
3.clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。clone和fork的调用方式也很不相同,clone调用需要传入一个函数,该函数在子进程中执行。此外,clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的
在这里插入图片描述
线程间共享独享的内容如上图;也就是说,线程间共享绝大部分的内容,但是硬件状态和栈是不能贡献的;

内核线程

Because some of the system processes run only in Kernel Mode, modern operating systems delegate their functions to kernel threads, which are not encumbered with the the unnecessary User Mode context.
有这么一部分的系统进程是只运行在内核态下的,操作系统将它们的函数委托给不受用户态上下文拖累的内核线程
内核线程和普通进程不同的是:
1.内核线程只运行在内核态,而普通进程会在用户态和内核态之间切换
2.内核态运行在内核地址空间(3G-4G),进程全占

对内核线程的创建,使用kernel_thread(),其实现也需要do_fork()和copy_process()的帮助;

所有进程的祖先是进程0,又称为idle进程,pid=0,它是Linux初始化阶段从无到有创建的第一个内核线程;其数据结构是由内核通过宏函数静态分配的;它通过start_kernel()函数初始化内核需要的所有数据结构,启动中断,通过调用kernel_thread()创建进程1也就是init进程;
进程0在创建进程1之后,执行cpu_idle()函数,这就是为什么叫它idle进程的原因;cpu_idle()函数的意义是,当没有其他进程处于TASK_RUNNING状态的时候,不断的重复执行hlt汇编指令;也就是说,当CPU没有要执行的进程的时候,进程0发挥作用,不停的hlt;

进程1在被进程0创建之后,执行init()函数;Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变成守护进程监视系统其他进程
也就是idle进程创建init进程,init进程创建所有其他进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值