进程 第一天....概念

一、Linux多任务机制

1、多任务机制简介

    多任务处理指的是用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为“任务”。相比于单任务的操作系统(例如早期的MS-DOS),当代绝大多数操作系统都支持多任务,功能增强了很多。

    但是,CPU(每个核心)在某一时刻只能执行一个任务,因此多任务操作系统必须解决CPU核心(单任务线性)与操作系统的任务(多任务并行)之间的矛盾。常见的解决方案是将CPU的运行分解成时间片(几十毫秒到上百毫秒不等),每个任务被分配不同的时间片来独占CPU进行运算。在该任务的时间片内,CPU被该任务独占,其他任务无法占用;在该任务的时间片外,CPU被其他任务独占,该任务也无法占用该CPU。由于CPU计算速度十分快且会频繁切换任务,因此用户感觉到当前操作系统是在“并行”的。

    因此,多任务操作系统需要解决各个任务间分配时间片的调度策略,对于某些重要的、耗时较长的任务需要多分配时间片,而对于不重要的、耗时较短的任务需要少分配时间片。

/***************一些常见的操作系统的任务(进程)调度算法*****************/

1)先来先服务(First Come First Served,简称FCFS)调度算法:最简单的任务/进程调度算法,该调度算法每次从当前运行进程的后备作业队列中选择一个或多个任务/进程并将其调入内存,分配资源。从表面上看,该算法对所有任务/进程都是公平的,不过该算法的缺点在于若有较长的作业流程的任务/进程正在工作,短作业流程的任务/进程需要等待很长时间。显然,该算法简单但总体效率较低,而且该调度算法对长运行时间作业有利,但对短运行时间作业不利。

2)短作业优先(Shortest Job First,简称SJF)调度算法:该调度算法每次从当前运行进程的后备作业队列中选择一个或多个运行时间最短的任务/进程,将其调入内存,分配资源。由于作业在未运行时无法事先知道实际运行时间的长短,因此该算法需要作业在提交申请的同时附带该作业运行时间的估算值。显然,该调度算法对短运行时间作业有利,但对长运行时间作业不利。

3)优先级调度算法:该调度算法基于需要运行的任务/进程的紧迫程度来进行调度,每次从当前运行进程的后备作业队列中选择一个或多个优先级最高的任务/进程并将其调入内存,分配资源。根据新的更高优先级进程能否抢夺当前正在执行的进程,可将该调度算法分为非剥夺式(无法打断)/可剥夺式(可以打断)两类。

4)最高响应比(Highest Response_ratio Next,简称HRN)调度算法:该调度算法是FCFS和SJF的一种综合平衡,响应比R的计算方法为:

R=(等待时间+预估运行时间)/预估运行时间

由此我们可以看出:

    1.当等待时间相同时,则预估运行时间越短,响应比越高,此时接近SJF,有利于短作业

    2.当预估运行时间相同时,则响应比由其等待时间决定,等待时间越长,响应比越高,此时接近FCFS

    3.对于较长运行时间的作业,作业的响应比可以随着作业的等待时间增加而逐渐提高,这样就可以一定程度克服进程调度的不公平的情况

5)时间片轮转调度算法:适用于分时系统,在这种算法中,将CPU的运行时间分解为时间片(几十毫秒到上百毫秒不等),每个进程都只能在对应的时间片内执行。时间片过后,即使该进程仍未完成也必须释放资源给下一个就绪的进程,被剥夺资源的进程重新排队等候再次运行。这种调度算法较为公平,不过若时间片切换过于频繁,则系统资源的开销会很大,因此选取合适的时间片是十分重要的。

6)多级反馈队列调度算法:该算法集合了上述所有算法的综合优点,通过动态调配进程优先级和时间片大小,可以实现兼顾多方面的系统任务/进程,同时无需事先预估任务/进程的运行时间。

/***************一些常见的操作系统的任务(进程)调度算法end**************/

2、任务

//任务、进程、线程的关系

任务是一个逻辑概念,指一个软件完成的活动,或者软件为了完成该活动/实现某个目的进行的一系列操作。通常情况下一个任务是一个程序的一次运行,一个任务可以包含一个或多个独立功能的子任务,通常情况下独立的子任务是进程或线程。

二、进程

1、进程的基本概念

进程(Process):进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行的过程,它是操作系统进行资源分配和调度的基本单元,是程序执行和资源管理的最小单位。

/************进程与程序的区别*******************/

进程与程序的区别有几点:

1)程序是静态的,它是保存在磁盘上的一些指令的有序集合,没有任何执行的概念;进程是动态的,它是程序执行的过程,包括创建、调度、消亡。

2)进程是一个独立的可调度的任务,是一个抽象实体,当系统在执行某个程序时,系统会分配和释放各种需要的资源。进程不仅包括程序的指令和数据,还包括程序计数器值、CPU寄存器值以及存储数据的堆栈等。

3)进程是一个程序的一次执行的过程。

4)进程是程序执行和资源管理的最小单位。

/************进程与程序的区别end****************/

2、进程的特性与分类

进程具有并发性、动态性、交互性和独立性等主要特性

    1.并发性:指的是系统内多个进程可以同时并发运行,互相之间不受干扰。

    2.动态性:指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是在不断变化的。另外进程具有动态的地址空间(包括代码、数据和进程管理块(Process Control Block,简称PCB)等。

    3.交互性:指的是进程在执行过程中可能会与其他进程发生直接或间接的通信,如进程的同步与互斥等,因此需要为进程添加相应的进程处理机制。

    4.独立性:指的是进程是一个相对完整的资源分配和调度的单位,各个进程的地址空间是相互独立的,只有采取特殊的手段才能实现进程间的通信。

Linux系统内主要包含以下几种类型的进程:

    1.交互式进程:这类进程用于操作系统与用户进行交互,需要用户的输入(键盘、鼠标等操作)。当接收到用户的输入后,该进程能立即响应,做出动作。常见的交互式进程有shell终端、文本编辑器(vim、emacs、gedit等)、图形化应用程序等。

    2.批处理进程:这类进程无需与用户进行交互,通常在后台运行。常见的批处理进程有编译器的编译操作、数据库搜索操作等。

    3.守护进程:这类进程一直在后台运行,与任何终端无关,通常情况下在系统启动时开始执行,系统关闭时才结束。许多系统进程(服务类进程)都是以守护进程的形式存在。

3、Linux下的进程结构

    因为Linux系统是一个多任务的操作系统,所以操作系统必须采取某种调度算法将处理器合理地分配给正在等待的进程。内核将所有进程存放在一个双向循环链表中,该链表的每一项都是task_struct类型的结构体,称为进程控制块。task_struct结构体内容很多,它能完整描述一个进程,如进程的状态、进程的基本信息、进程的标识符、内存相关信息、父进程信息、与该进程相关的终端信息、当前工作目录、当前打开的文件、所接收的信号等。

/***************task_struct结构体部分成员简介***********************/

//该结构体在/usr/src/linux-headers-3.2.0-29/include/linux/sched.h文件内,大约位于文件中部(1227行)

//不同虚拟机内该文件位置可能不同

1、进程状态

    volatile long state;

state成员用于描述进程的状态,可能的取值如下:

    TASK_RUNNING            进程正在运行或准备运行

    TASK_INTERRUPTIBLE        进程处在阻塞(睡眠)状态,等待某些事件发生。若被唤醒,则转变成TASK_RUNNING状态

    TAST_UNINTERRUPTIBLE    与前者类似,不过不会接收信号

    __TASK_STOPPED            进程被停止

    __TASK_TRACED            进程被debugger等进程监视

    EXIT_ZOMBIE                进程被终止,但是其父进程还未使用wait()函数族函数回收

    EXIT_DEAD                进程最终退出的状态

2、进程标识符

    pid_t pid;                进程标识符

    pid_t tgid;                线程组标识符(thread group id)

    其中pid表示进程标识符,在默认情况下,PID的取值范围是0~32767,即系统内进程最多有32767个。tgid表示的是线程组标识符,在内核运行多进程/多线程任务时,对于一个进程内的不同线程来说,每个线程都有不同的pid,但是有统一的tgid,线程组的领头线程的pid与tgid相同。当我们使用getpid()函数获取当前运行进程的进程号时,实际getpid()函数的返回值是tgid的值而不是pid的值。

3、表示进程亲属关系

    struct task_struct *real_parent;

    struct task_struct *parent;

    struct list_head children;

    struct list_head sibling;

    struct task_struct *group_leader;

其中

    real_parent                指向父进程,如果创建它的父进程已经不存在,则会指向init进程(PID为1的进程)

    parent                    指向父进程,当进程被终止时必须向父进程发送信号。通常该值与real_parent相同

    children                链表头结点,该链表内的元素都是该进程的子进程

    sibling                    当前进程的兄弟进程,该成员用于将当前进程信息插入到它的兄弟进程的链表内

    group_leader            指向所在进程组的领头进程

4、进程调度优先级

    int prio,static_prio,normal_prio;

    unsigned int rt_priority;

其中

    prio                    保存该进程的动态优先级

    static_prio                保存该进程的静态优先级,范围为MAX_RT_PRIO到MAX_PRIO-1(100~139),值越大优先级越低

    normal_prio                取决于静态优先级与进程调度策略

    rt_priority                保存该进程的实时优先级,范围为0到MAX_RT_PRIO-1(0~99),值越大优先级越低

5、运行时间

    cputime_t utime,stime;

二者都用于记录进程运行过程所经历的CPU定时器的节拍数,其中utime表示用户态,stime表示内核态。

6、构建进程链表

    struct list_head tasks;

7、文件IO相关

    struct fs_struct *fs;

    struct files_struct *files;

其中

    fs                        表示进程与文件系统的联系,包括文件的目录(当前目录/根目录)

    files                    表示进程打开的文件

8、中断使能

    struct irqaction *irqaction;

9、死锁检测

    struct mutex_waiter *blocked_on;

10、延迟计数

    struct task_delay_info *delays;

11、socket控制

    struct list_head *scm_work_list;

task_struct结构体还有许多成员,这里不再过多描述。有兴趣同学可以查阅内核手册了解更多

/***************task_struct结构体部分成员简介end********************/

在task_struct结构体内,我们最常使用的成员是state(进程状态)和pid(进程标识符)。

4、进程状态

Linux系统内的进程主要有以下几种状态

//运行状态及状态切换、

    1)运行状态(TASK_RUNNING):

    该状态下进程正在运行,或已经准备就绪等待调度

    2)可中断阻塞状态(TASK_INTERRUPTIBLE):

    该状态下进程出于阻塞(睡眠)状态,正在等待某些事件发生或等待分配某些系统资源。处在该状态下可以接收信号并被信号中断。当进程被唤醒(事件发生/获得资源/接收到某些信号/被系统显示唤醒)后,进程转换为TASK_RUNNING状态

    3)不可中断阻塞状态(TASK_UNINTERRUPTIBLE):

    该状态类似可中断阻塞状态(TASK_INTERRUPTIBLE),只不过该状态下进程不能接收或处理信号。在某些情况下(例如让进程必须等待直至事件发生/获得资源)这种状态是十分有用的。

    4)暂停状态(TASK_STOPPED):

    进程的执行过程被暂停。当进程收到某些信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号)时,就会进入该状态。当进程收到SIGCONT信号后,会恢复运行,进入TASK_RUNNING状态

    5)僵死状态(EXIT_ZOMBIE):

    进程的运行已经结束,但该进程的父进程尚未使用wait()函数族对其回收。处在该状态下的进程已经放弃了系统资源和内存空间,没有任何执行代码,也不能被调度,仅仅在进程队列内保留一个位置记载该进程的退出状态,等待父进程收集。

    6)消亡状态(EXIT_DEAD):

    父进程对该进程调用wait()函数族,该进程彻底退出。

5、进程标识符(PID)与父进程标识符(PPID)

    Linux内核采用进程标识符来标识每个进程(简称进程号,PID),PID存放在task_struct结构体的pid成员内。系统中可以创建的进程数目有限,默认情况下,Linux系统允许的最大用户进程数为32767,而单个进程允许的最大线程数为1024。(但受限于计算机性能,绝大多数情况无法达到最大进程数和最大线程数)

    当进程运行时,内核通常个会使用一个结构体指针current来索引该进程,例如current->pid表示当前处理器正在处理的进程的PID。

在Linux系统内,除了init进程(PID为1的进程)为内核启动时就存在,其余进程都是通过一个进程来创建另一个进程,被创建的进程称为“子进程”(child process),相应的,创建子进程的进程称为“父进程”(parent process)。因此,每个进程都有其相对应的父进程的进程标识符,称为父进程标识符(简称父进程号,PPID)。init进程是系统内其他所有进程的祖先。

    通常情况下,父进程需要负责子进程的资源回收工作。当一个子进程结束(调用exit()函数退出或者运行出现错误)时,子进程退出状态会上报给操作系统,操作系统再将该状态报告给该进程的父进程,由父进程负责子进程的资源回收工作。

    在Linux系统内,我们可以使用getpid()函数来获取当前进程标识符,使用getppid()函数来获取当前进程的父进程标识符。

6、进程的创建、执行与终止

1)进程的创建

Linux内的进程创建分为两步:

    1.调用fork()函数,复制当前进程信息创建一个子进程,父进程与子进程的区别仅仅在于PID、PPID和某些特殊资源(例如计时器等)

    2.调用exec函数族函数,读取可执行文件并将其载入地址空间开始运行

    由于调用fork()函数时,子进程需要复制父进程的资源(包括但不限于:代码区、数据区、堆区、栈区等),这样效率会十分低下。甚至,如果子进程要运行其他的可执行程序,则“拷贝父进程的资源”这个动作会毫无意义,所有的拷贝都会前功尽弃。因此Linux内核采用了写时拷贝技术(copy on write)来提高效率。

    写时拷贝技术:内核只为新生成的子进程创建虚拟空间,复制父进程的虚拟空间,但是不为这些虚拟空间分配物理内存,它们共享父进程的物理空间(即让这些虚拟空间指向父进程的实际内存),当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

    fork()函数还有一个兄弟函数:vfork()。有关于vfork()函数的使用方法见下篇笔记。

    exec函数族函数提供了在进程中启动另一个可执行程序的方法。exec函数族内的函数可以根据指定的文件名找到可执行文件(二进制文件/脚本文件),并替换原始进程内的数据区、代码区、堆区、栈区等。在执行完毕后,除了PID之外该进程的其他资源全部被替换成新的可执行程序。在fork()函数之后启动exec函数族函数可以装载其他程序运行,这样就可以让子进程运行与父进程不同的程序。

2)进程的终止

    终止进程时,系统需要做许多收尾工作,例如回收占用的系统资源、清理内存等并通知父进程该进程即将被回收。

    在终止进程时,系统会首先将该进程设置成僵死状态,此时进程无法运行,等待资源回收。僵死进程的存在仅仅为父进程提供信息,等待父进程在某个时间段调用wait函数族函数回收该进程使其进入退出状态。

我们可以使用exit()函数和_exit()函数终止一个进程,父进程可以使用wait函数族函数对子进程进行回收工作。

/**********孤儿进程***********/

若某个进程的父进程先于子进程结束,此时子进程还未结束,那么这个子进程就没有了父进程,变成了“孤儿进程”。

思考:若系统内出现了孤儿进程,则这个孤儿进程会被哪个进程“收养”?由哪个进程负责该进程的资源回收工作?

/**********孤儿进程end********/

7、用户空间与内核空间

    Linux系统管理内存的方式是“虚拟内存管理技术”,给每个进程分配独立的地址空间。这段地址空间是4GB的虚拟空间,用户所看到的、使用的内存均为虚拟内存地址,无法看到实际的内存地址,用户也无法直接访问物理内存。虚拟内存管理技术隔离了用户与内存,保障了内存的安全性。

    4GB的内存空间会被分隔成两部分——用户空间与内核空间。其中用户空间的地址为0~3GB(0x00000000~0xBFFFFFFF),内核空间为3GB~4GB(0xC0000000~0xFFFFFFFF)。内核空间内存放的是内核的代码与数据,用户无权访问。当用户使用系统调用函数或发生中断时,该进程就从用户空间切换到了内核空间。在内核空间内,终端往往无显示内容(或光标跳动),此时该进程无法对用户的命令做出响应,只有等待该进程从内核空间退出或强制终止该进程。

    当一个任务(进程)调用系统调用而进入内核空间时,我们称为该任务(进程)处于“内核态”,相应的,未处在内核空间而处在用户空间时,我们称为该任务(进程)处于“用户态”。

    例如,我们学习过的“使用文件IO函数操作文件”这个程序,当我们调用open()/close()/read()/write()/lseek()等系统调用函数时,此时该进程变成内核态,进入内核空间,系统内核会根据用户命令完成相应动作。当动作完成后,进程再切换回用户态,重新进入用户空间。

    用户空间与内核空间的不同点很多,例如用户空间内打印信息使用printf()函数,而内核空间内打印信息使用printk()函数。由于内核空间内的代码十分重要且与用户空间不同,因此在编写需要在内核空间运行的程序(例如驱动程序、模块程序等)时需要格外小心。

8、一些进程操作命令

    ps        查看系统内进程

    top        动态监测系统中进程

    nice    按用户指定优先级运行进程

    renice    改变运行中的进程优先级

    kill    杀死进程

    bg        将进程放在后台运行

    fg        将后台进程放到前台进行

猜你喜欢

转载自blog.csdn.net/nan_lei/article/details/81543251