首页 > 技术文章 > Linux进程管理

zhouqi0505 2019-07-26 15:22 原文

进程历史
进程是很重要的概念,进程实际是对计算机执行任务的封装。
最初的计算机是单进程的,一次只能有一个任务在执行。
单进程的计算机有一个问题,一个在执行的时候,另外一个必须等前一个执行完。
为了解决这个问题出现了多任务并发处理。现有linux进程的设计也是基于多任务并发处理系统。
我们看到linux的任务调度,任务切换机制都是保证多任务并发处理。
并发处理和并行处理有本质的不同:
1 并发处理是在一个cpu中分时间执行不同的进程。
2 并行处理是在多个cpu中同时处理不同的进程。
 
进程的概念
可以看作是cpu调度的最小单元,可以看成是一个正在运行的程序以及一些相关的资源的组合,进程内部有进行描述符来表示,一个进程描述符(task_struct)包含了一个进程的基本信息。
 
(1) 标示符 : 描述本进程的唯一标识符,用来区别其他进程。
(2 )状态 :任务状态,退出代码,退出信号等。 
(3) 优先级 :相对于其他进程的优先级。 
(4) 程序计数器:程序中即将被执行的下一条指令的地址。 
(5) 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
(6) 上下文数据:进程执行时处理器的寄存器中的数据。 
(7)  I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。 
(8)  记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
 
链接表通过程序计数器来实现。程序计数器决定了一个程序在中断之后该从哪里继续执行。内存上下文,和上下文数据同样重要,恢复执行的时候要有这两个东西,才能够顺利恢复。
 
进程的优先级
在进行进程调度的时候,优先级具有重要的作用,一般进程分为实时进程和普通进程。实时进程的优先级比普通进程高。优先级有140个等级用来表示进程的优先级,在进程调度中会详细阐述这些概念。
 
进程的生命周期管理
进程的生命周期分为几个阶段:
1 运行: 进程正在运行中。
2 等待: 进程能够运行,但需要cpu的调度器调度这个进程。
3 睡眠: 进程无法执行,需要达到某一个条件才行。
 
内核把进程的列表存放在一个叫做任务队列的双向链表中。链表中的每一个项的类型为task_struct, 被称为进程描述符。
 
进程的状态及其转换
 
 
 
命名空间(namespace)
Namespace, 对资源进行隔离,docker的隔离就是使用namespace技术。namespace可以隔离网络协议
栈,可以隔离文件系统,也可以隔离用户。进程中有一个命名空间的字段,用来表示进程所属的命名空间。
 
而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。
 
上面这句话表明了虚拟机和容器的本质区别,也是容器技术建立的基础。虚拟机是运行的多个内核,使用指令陷入模拟的方式来进行隔离。而不同的容器使用的是同一个内核,但一些关键的资源如用户,内核网络协议栈等技术使用命名空间来隔离。
 
命名空间具有结构性,如下图所示:
 
命名空间的实现:
  • 每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中(不理解)
  • 将给定进程关联到所属各个命名空间的机制
 
每一个进程描述符中都有一个struct nsproxy数据结构,数据结构
<nsproxy.h>
struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns;
    struct user_namespace *user_ns;
    struct net *net_ns;
};
 
这个struct里面的具体的数据结构指向的是namespace.
uts_namespace记录了内核系统相关的信息,系统名称,系统版本,底层体系结构等相关信息。
ipc_namespace记录了所有与进程通信有关的信息。
mnt_namespace隔离了文件系统。
pid_namespace隔离了进程ID。
user_namespace隔离了用户相关信息。
net_ns与网路资源有关。
 
进程关系
进程并不是孤立存在的,而且有父进程和兄弟进程。
进程为什么要基于父进程生成,而不是直接创建一个新的进程。这个其实涉及到程序或者进程的运行方式,计算机在运行的时候,有很多进程或者程序在运行。但是某个程序在运行的时候需要分裂出几个子进程来协同工作。比如httpd服务,可能分裂出几个子进程来协同处理。这个其实是模拟了现实生活中的组织关系。
 
一家公司有财务部,销售部和市场部。财务部有几个人,每个人可以看作一个进程,市场部有几个人也可以看作一个进程。整个财务部门可以看作是一个父进程,专门用来处理财务相关事物。但涉及到具体事物时,又要分给具体的某个人进行处理,比如有的人主要负责发工资,有的人负责出纳。想想为什么建立财务部,又为什么把从事财务相关的人员挂在财务部。就可以想清楚为什么子进程要有父进程。
 
进程管理相关的系统调用
系统调用就是linux内核提供接口让应用使用内核的相关功能。
Linux进行进程复制的系统调用有三个
fork
vfork
clone
 
写时复制(COW)
fork进行系统调用的时候复制了父进程的所有数据,这样做负面效应会比较严重。
1 完全复制占用地址空间
2 完全复制耗费时间,而且复制的内容有可能用不到。
基于以上的原因,linux采取了写时复制的技术,只复制页表,不复制具体数据。
 
写时复制是怎么实现的?下面的文章解释的比较清楚。
fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
Copy On Write技术好处是什么?
* COW技术可减少分配和复制大量资源时带来的瞬间延时。
* COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
Copy On Write技术缺点是什么?
* 如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。
 
cow有它的优点可以减少不必要的资源和时间的浪费,但是这种技术也有它的缺点。
 
思考:
不过现在来看linux大量采用这种技术说明它是优点大于缺点的,任何的解决方案或者技术都有它的优点和缺点,不能一概而论。如果它的优点明显大于缺点那么就可以采用这种技术。至于缺点尽可能采取其他的方案进行规避。在思考方案的时候需要进行权衡,不能引入比现有问题还严重的问题。
 
fork的 exec调用指的是什么?
exec()的作用是: 装载一个新的程序覆盖当前进程内存空间中的映像,从而执行不同的任务。exec系列函数在执行时会直接替换掉当前进程的地址空间。所以如果是程序在执行完fork之后,直接执行exec系列函数,那么完全拷贝的数据几乎是浪费的。
 
执行系统调用
系统调用是用户态专向内核态的入口,执行系统调用其实就是调用C语言的库来进行开启新进程的操作。
需要进一步学习
 
内核线程
1 内核线程由其他内核进程创建,与普通进程一样可以进行调度。
2 内核线程运行在内核态,只访问内核内存地址。
3 内核线程用于执行以下任务:
  • 周期性地修改内存页与页来源块设备同步。
  • 管理延时任务。
  • 实现文件系统事物日志。
4 linux驱动模块中可以用kernel_thread(),kthread_create()/kthread_run()两种方式创建和调用内核线程
5 内核线程分为两种类
  • 启动后一直运行。
  • 启动后周期性执行。
6  执行 ps fax 查看带中括号的就是内核线程。
7 内核进程和普通进程的区别:
  •  内核线程没有地址空间
  • 内核线程只存在内核中,因此不可能转换到用户态。
 
 
启动新程序
新程序的启动过程需要掌握,程序的链接,装载是怎么实现的?
启动新程序的系统调用是sys_execve函数,该函数委托给
do_execve()
 
参考资料
《linux内核设计与实现》
https://blog.csdn.net/gatieme/article/details/51594439
https://blog.csdn.net/huangweiqing80/article/details/83088465
 
 
 
 
 

推荐阅读