进程_进程间通信 - CSDN
  • 什么是进程

    2018-09-01 10:25:05
    进程:进行就是正在进行中的程序  1、用户角度: 进程是程序的一次动态执行过程  2、操作系统: 进程是操作系统分配资源的基本单位,也是最小单位 很抽象的概念,那么,到底什么是进程呢?又为什么要有进程? ...

     

    程序:完成特定任务的一系列指令集合 

             代码段+数据段

             -----放在磁盘中的程序 

    进程:进行就是正在进行中的程序

             1、用户角度: 进程是程序的一次动态执行过程

             2、操作系统: 进程是操作系统分配资源的基本单位,也是最小单位

    很抽象的概念,那么,到底什么是进程呢?又为什么要有进程?

    CPU一次只能处理一个程序,CPU速度很快,而内存很慢,所以CPU会有大量的时间都是空闲的。而CPU又是很昂贵的,为了解决浪费CPU的情况,就出现了中断处理,将程序分成一小片一小片的,这个进程执行一点,那个进程执行一点。虽然在内部进程的执行是一段一段的,但是CPU的速度很快的(速度都是纳秒级别的),所以我们是感受不到进程执行过程中的停顿的。

    CPU执行程序的过程

     

           既然是一段一段执行,CPU在执行一个进程时,其他的进程就处于等待状态,为什么执行当前的这个进程而不是其他进程呢?这就要分情况讨论了,有可能是因为轮到它们的时间片用完了,也可能是它在等待某种资源等,所以在内存中,每个进程的状态可能是不同的,那么,进程的状态有哪些呢?

    再分详细一点的话就有七态了

          

     

    现在的系统都是多道程序处理系统,那就代表着在同一段时间内,系统里边执行的程序可能有很多个,那么,系统通过什么去区分他们呢?

     

    进程的标识号:

    系统为每一个进程分配一个唯一的标识号(正整数),这个标识号就称为进程ID(progress identifer),即PID。最早的时候有一个0号进程,是写死在内核里边的,所以操作系统在启用的时候,0号进程就启用了。

    0号进程的作用:启动1号进程,将1号进程启动了之后就没事干了

    1号进程是系统启动的第一个用户态进程,其他进程就相当于1号进程的子孙。

    所以之后我们创建的进程的最小为2号。

    想象一下一个小区里边,每套房都有一个门牌号(相当于我们的PID),为什么仅凭一个房间号就能确认哪个是你自己的家,而不会走到你邻居家呢?因为每一个门牌号都有属于自己的空间,所以每个进程也应该要有属于自己的地址空间。这个空间给多大呢?4G,Linux下有3G在用户空间,1G在内核。Windows下,有2G在内核,2G在用户空间。每个进程都有4G,同一时间内可能会有成百上千乃至上万个进程,累加起来,所需的内存是非常庞大的,然而我们的计算机内存并没有那么多的内存(内存可是很贵的),所以,进程的地址空间其实是虚拟地址,是操作系统给它们画的一个大饼。

           前边已经说了,进程是一小片一小片的执行的,所以,即使进程认为自己拥有4G空间,但是并不会在同一时刻使用4G空间,那么,操作系统只需要在你需要使用的时候给你分配一点空间就行了。你不用的时候,操作系统就可以将这块空间给别的进程使用。那怎么通过虚拟地址找到对应的物理地址(在内存中真时存在的地址)呢?操作系统为每个进程提供了一个MMU(内存管理单元),里边有一张表,里边将虚拟内存映射到物理内存的情况记录着,然后每个进程里边都有一个指针(在mm_struct 里边的pgd指针)指向这张表

    除了以上讲的进程pid地指空间,进程在执行过程中,可能会打开文件等资源,还有进程在切换的时候,当前的状态和它执行到哪了(上下文信息)这些信息是不是都应该记录下来呢?那这些信息都存放在哪呢?

    接下来要提出一个进程中最重要的概念,PCB(process control block)即,进程控制块。既然是控制进程的,进程的相关信息都应该存放在里边。那么PCB到底是何方神圣呢?其实PCB的本质就是一个名为task_struct 的结构体。里边存放着进程几乎所有的信息。

    task_struct(PCB)内容分类

     

    标识符:就是进程的PID。

    状态:任务状态、退出代码,退出信号等。

    优先级:相对于其他进程的优先级。

    程序计数器:程序中即将被执行的下一条指令的地址。

    内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

    上下文数据:进程执行时处理器的寄存器中的数据。

    I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

    记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

     

    PCB可以说是很强大的了,那么,操作系统想要控制一个进程,只需控制其PCB就行了。所以操作系统控制一个进程分两步:

            1、通过PCB将一个进程描述起来

            2、将这些PCB组织起来。

    怎么将这些PCB组织起来呢?前边已经说了,PCB是一个结构体,那么创建一个链表,每个节点都存放一个PCB,来一个进程,就把他的PCB挂到链表上,来一个挂一个,那么,操作系统只要拎着这个链表的头,后边所有的PCB都可以获取到了,PCB获取到了,整个进程的控制权也就拿到了。

           进程有不同的状态,如果不同状态的进程都放在一个链表上的话,如果进程很多,链表就会很长,这就降低了操作系统调度在对进程进行调度时的查找效率。为了解决这个问题,操作系统创建了索引表。让同一状态的进程归入一个索引表再由索引指向PCB,多个状态对应多个不同的索引表,各状态的进程形成不同的索引表,就绪索引表、阻塞索引表……

    展开全文
  • 文章目录一、操作系统对进程的管理1、了解进程2、操作系统对进程的控制方式二、进程的状态 一、操作系统对进程的管理 1、了解进程 (1)概念 通俗一点来理解,进程就是运行中的程序,这个运行是指的把程序从磁盘中...

    一、操作系统对进程的管理

    1、了解进程

    (1)概念
    通俗一点来理解,进程就是运行中的程序,这个运行是指的把程序从磁盘中加载到了内存上。
    但是具体的来说,进程并不仅仅局限于一段可执行的程序代码(我们称其为代码段)。通常进程还包含了其他资源,像打开的文件,挂起的信号,内核内部数据,处理的状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。所以说进程时一组有序指令+数据+资源的集合。实际上,进程就是正在执行的程序代码的实时结果

    (2)进程中的活动对象
    线程是进程中的活动对象。每一个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。在现在的操作系统中一个进程可以包含多个线程,对于Linux而言,线程只不过是一种特殊的进程。

    (3)存在两个或多个不同的进程进程执行同一个程序
    进程在创建的时候开始存活,在Linux系统中调用fork()复制一个现有的进程来创建一个全新的进程。在调用结束时,在返回点这个相同的位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间。

    2、操作系统对进程的控制方式

    操作系统通过PCB记录进程的相关属性
    (1)初识PCB
    PCB又叫做进程描述符,都是类型为task_struct,该结构定义在<linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。
    task_struct相对较大,在32位机器上,他大约有1.7KB。进程描述符中包含的数据能完整地描述一个正在执行的程序:他打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息,如下图所示:
    在这里插入图片描述
    另外,内核把进程的列表存放在叫做任务队列的双向循环链表中,具体图示如下。Linux通过slab分配器分配task_struct结构。
    在这里插入图片描述

    (2)进程的生成和回收

    进程新生成时,必须先分配PCB结构,后生成进程主体。内核通过一个唯一的进程标识值或PID来标识每个进程,PID是一个数,最大值默认设置为32768(short int短整型的最大值)内核把每个进程的PID存放在他们各自的进程描述符中。
    进程结束时,先释放主体,然后才会释放PCB的结构。在这里释放PCB需要一定的条件,比如说退出码等,所以就有了我们所说的僵死进程和孤儿进程的出现。
    僵死进程:进程主体释放,但是PCB依旧保留。僵死进程的存在是有危害的,就像我们之前描述的那样,PCB在内存占1.7k的空间,大量的僵死进程存在会导致内存太满。
    孤儿进程:父进程已经结束,但是子进程还没有结束。系统将所有的孤儿进程都挂载在INIT下。

    二、进程的状态

    1、三种状态
    最简单的进程可以划分为三个状态,包括运行、就绪和阻塞态。运行是指CPU正在执行进程中的指令,就绪是指等待CPU执行的进程,阻塞是指等待I/O事件发生的。他们的关系如下图所示:
    在这里插入图片描述
    2、五种状态
    (1)TASK_RUNNING(运行):表示它或者正在执行或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态,这种状态也可以应用到内核空间中正在执行的进程。
    (2)TASK_INTERRUPTIBLE(可中断):进程被阻塞,等待某些条件的达成把进程状态设置为运行。
    (3)TAK_UNINREREUPTIBLE(不可中断):就算接收到信号也不会唤醒或准备投入运行外,这个状态与可打断状态相同
    (4)_TASK)TRACED:被其他进程跟踪的进程
    (5)_TASK_STOPPED(停止):进程没有投入运行也不能投入运行。
    他们的具体转换图如下:
    在这里插入图片描述
    3、七种状态
    在这里插入图片描述
    在这里插入图片描述
    在这里,主要强调一下退出状态,有四种退出方式分别是:
    (1)正常退出(自愿的):进程由于完成了他们的工作而退出,比如说调用exit.
    (2)出错退出(自愿的):通常是由于程序中的错误导致,例如执行一条非法的指令、引用不存在的内容或除数是零。在这类错误中,进程会收到信号被中断
    (3)严重错误(非自愿):比如说要编译一个不存在的程序时。
    (4)被其他进程杀死(非自愿):在Linux系统中,系统调用kill.

    展开全文
  • 进程的基本概念

    2018-11-15 13:39:26
    1.什么是进程?  1. 进程就是运行起来的程序,程序运行起来需要被加载到内存中。(这是站在用户的角度看待进程的)  2. 进程就是操作系统的描述,这个描述叫PCB(进程控制块),Linux下PCB有自己的名字叫task_...

    1.什么是进程?

          1. 进程就是运行起来的程序,程序运行起来需要被加载到内存中。(这是站在用户的角度看待进程的)
          2. 进程就是操作系统的描述,这个描述叫PCB(进程控制块),Linux下PCB有自己的名字叫task_struct.而操作系统就是使用task_struct结构体描述进程,使用双向链表来将这些结构体组织起来进行管理。

    task_struct结构体的内容分类(进程的描述信息)下面简单的介绍几类:

        进行(运行)中的程序------程序是死的。放在硬盘中,当运行起来>的时候就会被加载到内存中。        站在操作系统的角度:进程是PCB(进程控制块) ---task struct
                    标识符:描述本进程的唯一标识符,用来区别其他进程的。
                    状态:任务状态,退出代码,推出信号等。
                    上下文数据:进程执行时处理器的寄存器中的数据。
                    程序计数器:记录每个进程下一次要进行的指令的地址。
                    优先级:相对于其他进程的优先级。

                    文件的状态信息
                    记账信息。

    如果想深入的了解的话可以查看大佬的详解:https://blog.csdn.net/bit_clearoff/article/details/54292300

    2.查看进程信息的命令

            ps -ef:查看所有的进程信息。
            ps aux:查看进程的详细信息。
            top;(查看进程的信息)
            查看你所要的进程信息: ps -ef | 名字。
                    getpid();(在代码中获取一个进程的id)(一个系统的调用接口)

    #include<stdio.h>
    
    int main()
    {
        printf("%d\n", getpid());//使用getpid()来获取一个进程的id
        return 0;
    }

    3.如何创建一个进程:

            shell---命令行解释器(shell相当于我们命令行的界面,如果我们从键盘中敲入一个指令时,shell就会执行这条指令)。

    系统调用open返回的是一个文件描述符,类型为int,C库中的fopen返回的是文件流指针:FILE*。
    我们所说的缓冲区(printf),用户态的一个缓冲区,是文件流指针自带的
        printf,  fwrite这些库函数都是先把数据写入到缓冲区中,等缓冲区写满了之后或者其他;条件满足之后才会写入到真正的文件中,而系统调用没有这个用户态的缓冲区(是直接将数据写入到文件中)这个说法不准确
    fork() ---(通过复制调用生成一个新的进程)创建一个子进程。
                    对于父进程来说,fork()的返回值是子进程的pid
                    对于子进程来说,fork()的返回值是0

            因为子进程是根据父进程为模板来创建的,因此父子的代码段是一样的(父子进程运行的是同一段代码)。但是父子进程的返回值不同。父子进程的数据不相同。子进程的数据会另外开辟内存来存放。
            对于代码来说父子进程是相同的(相同的代码只是fork()之下的代码,而不是从头到尾),但是父子进程的数据是独有的(写时复制技术)。
            父进程是从代码头到代码的结束。子进程是从fork()开始到代码结束。
            用户就是通过返回值来判断分辨父子进程,来进行代码的分流。

            kill 进程号----》杀死进程。

    4.进程的状态

            R(running)            运行态:并不意味着程序一定在运行中,它表明进程要么在运行中要么在运行队列里。
            S(sleeping)           可中断的休眠(浅度睡眠)
            D(disk sleep)         不可被中断的休眠,只能通过指定的方式--->唤醒(深度睡眠)
            T(stopped)            停止的状态
            t(tracing stop)       追踪状态
            X(dead)                 死亡状态
            Z(zombie)             僵死态

    5.僵尸进程

            产生原因:
                    子进程先于父进程退出它会保存自己的退出状态,因此它不会自动释放所有资源。子进程退出后会通知父进程子进程退出了,然后让父进程去获取退出状态,然后完全释放子进程资源。假如父进程不管子进程的退出状态,那么这个子进程就会变成一个僵死进程。
            僵尸进程的危害:资源泄露,正常的进程可能无法创建。
            僵死进程如何杀死:杀死它的父进程就可以了。

    下面是一个僵尸进程的演示:

    #include<stdio.h>
    #include<stdlib.h>
    
    int main()
    {
            pid_t pid = fork();
            if(pid < 0)
            {
                    perror("fork error");
                    return -1;
            }
            else if(pid == 0)
            {
                    printf("this is a child\n");
                    sleep(10);//为了方便我们观察子进程退出之前的状态
                    exit(0);
            }
            else
            {
                    sleep(30);//为了使子进程先于父进程退出
                    printf("this is parent\n");
            }
            while(1)
            {
                    sleep(1);
            }
            return 0;
    }
    

    当子进程还没有退出的时候:此时的子进程和父进程都没有退出。(使用ps aux查看进程的状态)

     当子进程退出的时候:因为父进程还没有退出的时候,而子进程退出了,所以子进程就变成了一个僵尸进程。如下所示:(第一个是父进程,第二个是子进程(现在变成了僵尸进程))。

     上面这个就是一个僵尸进程。

    6.孤儿进程

            产生的原因:
                    父进程先于子进程退出,那么这个子进程就会变成一个孤儿进程,并且进入后台运行。
            特性:
                    它原来的父进程退出后。它的父进程就变成了init进程,如果子进程退出后,init进程将负责释放子进程的资源,所以子进程就不会变成一个僵死进程。
            孤儿进程运行时,是后台运行,你可以继续输入其他的指令。

    下面是孤儿进程的代码演示:

    #include<stdio.h>
    #include<unistd.h>
    
    int main()
    {
            pid_t pid = fork();
            if(pid < 0)
            {
                    return -1;
            }
            else if(pid == 0)
            {
                    sleep(10);//让子进程睡的时间长一点,让父进程先退出
                    printf("this is child\n");
            }
            else
            {
                    sleep(5);//这里睡几秒是为了执行后观察父进程退出之前的状态
                    printf("this is parent\n");
            }
            return 0;
    }
    

    当父进程退出之前的状态:此时的父进程和子进程都没有退出:

    当父进程先于子进程退出的时候:父进程因为退出而消失,子进程就变成了一个孤儿进程。

    这就是一个孤儿进程。

    7.进程的优先级

     为什么要有进程的优先级?

             因为进程的功能不同,因此进程对CPU的资源的要求也不同,所以对进程的调度就有了优先级,进程的优先级决定以一个进程的CPU资源的优先级分配权。

             cpu资源分配的先后顺序,就是指进程的优先权。

    使用命令ps -l来查看

    上面的一些列是什么意思呢?

            UID:代表执行者的身份
            PID:代表这个进程的id
            PPID:代表的是父进程的id
            PRI:代表这个进程可被执行的优先级,它的值越小优先级越高
            NI:代表这个进程的nice值。(表示进程可被执行的优先级的修正数据)      

    其实进程的优先级最后等于PRI(New) = PRI(Old)+ nice  (所以当nice的值是负数的时候,优先级越高)。    PRI越小,越早执行。

    修改进程的优先级命令:

            nice:可以指定进程(还没有运行起来的进程)的优先级。(nice -n 你要设置的nice值  程序)nice -n -10  ./test
            renice:也可以指定进程(已经运行起来的进程)的优先级。(renice 你要设置的nice值   -p   进程的id)renice -10 -p 5200

    修改演示:首先编写一个死循环的程序,然后执行这个程序

    nice 对进程的修改:

    renice 对进程的修改:首先将程序执行起来,然后找到这个进程的id,然后用renice来修改优先级。

    其他的概念:

            竞争性:系统的进程数目过多,而CPU只有少量,甚至只有1个,所以进程之间具有竞争的属性,为了高效的完成任务,更合理的竞争相关的资源,所以就有了优先级。
            独立性:一个进程出现问题不会影响其他的进程。多个进程运行期间互不干扰。
            并行:多个进程在多个CPU下分别同时运行,这称之为并行。
            并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

    环境变量

            概念:环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。

    查看环境变量的命令:env.

            环境变量保存操作系统环境相关的一些功能性的数据的变量。
            环境变量具有全局性,就是在哪里都可以找到这个环境变量。

    环境变量的一些操作:

            1.通过main函数的第3个参数来获取环境变量。

    #include<stdio.h>
    #include<unistd.h>
    
    int main(int argc, char *argv[], char *env[])
    {
            int i = 0;
            for(i = 0; env[i] != NULL; i++)
            {
                    printf("%s\n ",env[i]);
            }
        return 0;
    }
    

            2.通过一个全局变量**environ来获取环境变量。

    #include<stdio.h>
    include<unistd.h>
    int main(int argc, char *argv[], char *env[])
    {
       extern char **environ;//存放环境变量的全局变量(因为它没有包含在头文件中,所以要声明)
       int i = 0;
       for(i = 0; environ[i];i++)
       printf("%s \n",environ[i]);
       return 0;
    }
    

            3.getenv() ---》-获取指定的环境变量。(getenv("PATH")).
                              putenv()-----设置一个环境变量

    #include<stdio.h>
    #include<unistd.h>
    #include<stdlib.h>
    int main()
    {
            printf("%s\n", getenv("PATH");//用getenv来访问PATH这个环境变量
            return 0;
    }
    

            4.export (设置一个环境变量)。

            5.set :查看一个环境变量。(它查看的环境变量比env更加的详细)
            6.echo :可以打印一个环境变量,在变量前加$.
            如何干掉一个环境变量:
                 unset:删除一个环境变量(unset 环境变量的名字)

    程序(进程)地址空间:

            首先它是一个线性的地址空间。(进程的虚拟地址空间)
            程序没有地址空间,因为程序在被激活运行起来之后才会有一个运行空间。

            内存地址就是对内存的一个编号,一个地址指向内存的一个位置。

    那我们可以直接访问到物理内存呢?看下面的两段代码,思考为什么?

    zone1.c

    #include<stdio.h>
    #include<unistd.h>
    
    int val = 100;
    
    int main()
    {
            pid_t pid = fork();
            if(pid < 0)
            {
                    return -1;
            }
            else if(pid == 0)
            {
                    printf("child val:%d----%p\n",val);//在子进程中打印出val的值和它的地址
            }
            else
            {
                    printf("parent val:%d----%p\n", val);//在父进程中打印出val的值和他的地址
            }
            return 0;
    }
    

    zone.c

    #include<stdio.h>
    #include<unistd.h>
    
    int val = 100;
    
    int main()
    {
            pid_t pid = fork();
            if(pid < 0)
            {
                    return -1;
            }
            else if(pid == 0)
            {
                    val = 200;//在子进程中修改val的值,然后查看父进程中的val的值是否改变
                    printf("child val:%d----%p\n",val);//在子进程中打印val的值和地址,
            }
            else
            {
                    sleep(3);//这里让父进程先睡三秒,让子进程先执行。
                    printf("parent val:%d----%p\n", val);//在子进程中打印val的值和地址,查看与父进程中的val和地址有什么区别。
            }
            return 0;
    }
    

    这两个代码段执行的结果为: 

    这是就会产生一个问题:为什么在zone的执行结果中,父子进程的变量的值不相同,但是地址为什么相同呢?

            1.变量的内容不一样,代表父进程和子进程输出的变量绝对不是同一个变量

            2.地址一样,代表该地址一定不是物理地址。

    其实进程的地址空间是一个虚拟的地址,并不是物理内存的地址。

    一个虚拟地址如何找到真正的物理地址?

            其实虚拟地址和物理地址直接有一个页表的存在。

            页表:1.记录虚拟地址与物理地址之间的映射关系。
                       2.内存的访问控制(记录了属性的信息,比如:记录了该代码段是只读或者是只写的)。

    页表也是一个结构体。

    什么是虚拟地址空间:
            虚拟地址空间是一个结构体mm_struct结构描述的,结构体的名字叫struct_mm,因此程序地址空间应该叫虚拟地址空间。

    程序的地址空间都是虚拟地址,而不是真正的物理内存地址,而访问虚拟地址空间是通过页表的转换后得到物理内存地址而访问内存的。

    为什么计算机告诉我们每个进程都有4G的地址空间?
            因为计算机知道我们用不了这门多的地址空间,所以我们使用多少就通过页表给我们在物理内存中映射多少地址空间。

    写时复制技术:
            假设有一个全局变量,那么在物理内存中就有一段空间来存放这个全局变量,如果父子进程不修改这个全局变量的话,那么父子进程的虚拟地址空间通过页表都指向同一块物理空间。如果父子进程修改这个全局变量的话,虚拟内存的地址不变,但是在物理内存空间会重新分配一块内存来保存你父子进程修改后的数据,并更新页表,这样也就是为什么你发现父子进程中一个变量它的虚拟地址相同但是值不同原因。

    展开全文
  • 非常详细的linux进程知识点!图文并茂

    进程

    说明:本文虽出现在野火书籍中,但作者却是本人~

    简单了解进程

    在进入正题之前,我们不打算像其他书籍一样,讲一大堆原理,而是希望想通过实验现象来引导读者了解进程相关的知识,紧接着再来补充进程相关的知识点。

    首先在虚拟机中打开一个终端(相信读者能阅读到此处,就已经了解什么是终端了,而由于作者用是的公司的服务器,使用ssh方式连接的,所以下文的截图有可能与读者的显示界面不是一样的,但这不会对本章的阅读造成任何影响),一般来说ubuntu中有很多shell终端,而这个终端就是一个进程,或许读者很可能打开多个终端,那么这些所有被打开的终端就一个个进程,这些进程是不一样的,独立运行在系统中,作者打开三个shell终端,这些终端各自有输入输出,互不干扰,如图37‑1所示。

    proces002

    图 37‑1 三个shell终端

    每个运行中的 shell 都是一个单独的进程,假如读者从一个 shell里面调用一个程序的时候,对应的程序在一个新进程中运行,运行结束后 shell继续工作,高级的程序员经常在一个应用程序中同时启用多个协作的进程以使程序可以并行更多任务、使程序更健壮,或者可以直接利用已有的其它程序,这样子整个系统中运行的进程就可以相互配合,而不是独立运行,这就涉及到进程间的通信,这部分内容我们在后续讲解。

    查看进程

    即使读者刚打开linux电脑,没有运行任何程序,电脑中也会有进程存在,因为系统中必须要有进程在处理一些必要的程序,以保证系统能正常运行。其实在Linux中是通过检查表记录与进程相关的信息的,进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过进程的ID对它们进行管理,这些PID是进程表的索引,就目前的linux系统而言,系统支持可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数量的限制,也就是说系统有足够的内存的话,那么理论上就可以运行无数个进程。

    进程ID

    Linux系统中的每个进程都都会被分配一个唯一的数字编号,我们称之为进程ID(ProcessID,通常也被简称为 PID)。进程ID 是一个 16位的正整数,默认取值范围是从2到32768(可以修改),由 Linux在启动新进程的时候自动依次分配,当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当PID的数值达到最大时,系统将重新选择下一个未使用的数值,新的PID重新从2开始,这是因为PID数字为1的值一般是为特殊进程init保留,即系统在运行时就存在的第一个进程,
    init进程负责管理其他进程。

    父进程ID

    任何进程(除init进程)都是由另一个进程启动,该进程称为被启动进程的父进程,被启动的进程称为子进程,父进程号无法在用户层修改。父进程的进程号(PID)即为子进程的父进程号(PPID)。用户可以通过调用getppid()函数来获得当前进程的父进程号。

    为了更直观看到这些进程,作者就使用ps命令去查看系统中的进程情况,ps命令可以显示我们正在运行的进程、其他用户正在运行的进程或者目前在系统上运行的所有进程。输出结果如图37‑2所示,可以很明显看到,编号为1的进程是init进程。它位于/sbin/init目录中。当然,整个系统的进程可不止这一些,由于太多没法截图,就只展示这小部分的进程,读者可以自己使用下面命令去尝试一下,ps 命令可以显示当前系统中运行的进程, 其实在linux中, ps命令有很多选项,因为它试图与很多不同 UNIX 版本的 ps命令兼容,这些选项决定显示哪些进程以及要显示的信息。

    命令:

    ps –aux
    

    proces003

    图 37‑2 ps –aux输出结果

    父进程与子进程

    进程启动时,启动进程为新进程的父进程,新进程是启动进程的子进程。

    每个进程都有一个父进程(除了系统中如“僵尸进程”这种特殊进程外),因此,读者可以把 Linux
    中的进程结构想象成一个树状结构,其中 init进程就是树的“根”;或者可以把init进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的。

    总的来说init进程下有很多子进程,这些子进程又可能存在子进程,就像家族一样。系统中所有的父进程ID被称为PPID,不同进程的父进程是不同的,这个值只是当前进程的父进程的ID,系统中的父进程与子进程是相对而言的,就好比爷爷<->爸爸<->儿子之间的关系,爸爸相对于爷爷而言是儿子,相对于儿子而言则是爸爸。

    为了更加直观看出系统中父进程与子进程,作者决定使用pstree命令将进程以树状关系列出来,具体见图
    37‑3。

    命令:

    pstree
    

    proces004

    图 37‑3 pstree命令结果

    程序与进程

    进程相关信息也简单了解过了,可能很多读者还是有疑问,我们写的代码,它是程序,怎么变成进程了呢?那么在本小节作者就讲解一下程序与进程的关系。

    程序

    程序(program)是一个普通文件,是为了完成特定任务而准备好的指令序列与数据的集合,这些指令和数据以“可执行映像”的格式保存在磁盘中。正如我们所写的一些代码,经过编译器编译后,就会生成对应的可执行文件,那么这个就是程序,或者称之为可执行程序。

    进程

    进程(process)则是程序执行的具体实例,比如一个可执行文件,在执行的时候,它就是一个进程,直到该程序执行完毕。那么在程序执行的过程中,它享有系统的资源,至少包括进程的运行环境、CPU、外设、内存、进程ID等资源与信息,同样的一个程序,可以实例化为多个进程,在Linux系统下使用 ps命令可以查看到当前正在执行的进程,当这个可执行程序运行完毕后,进程也会随之被销毁(可能不是立即销毁,但是总会被销毁)。

    程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,这种执行的程序称之为进程,也就是说进程是系统进行资源分配和调度的一个独立单位,每个进程都有自己单独的地址空间。

    举个例子,我们可以看到/bin目录下有很多可执行文件,如图37‑4所示,我们在系统中打开一个终端就是一个进程,这个进程由bash可执行文件(程序)实例化而来,而一个linux系统可用打开多个终端,并且这些终端是独立运行在系统中的。

    proces005

    图 37‑4 /bin目录下的可执行文件

    程序变成进程

    在linux系统中,程序只是个静态的文件,而进程是一个动态的实体,进程的状态(后续讲解进程状态)会在运行过程中改变,那么问题来了,程序到底是如何变成一个进程的呢?

    其实正如我们运行一个程序(可执行文件),通常在 Shell中输入命令运行就可以了,在这运行的过程中包含了程序到进程转换的过程,整个转换过程主要包含以下
    3 个步骤:

    1. 查找命令对应程序文件的位置。
    2. 使用 fork()函数为启动一个新进程。
    3. 在新进程中调用 exec 族函数装载程序文件,并执行程序文件中的main()函数。

    补充:关于具体的函数介绍将在后续讲解。

    总结

    总的来说,程序与进程有以下的关系:

    1. 程序只是一系列指令序列与数据的集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行过程,它是一个动态运行的实体,有自己的生命周期,它因启动而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被销毁。

    2. 进程和程序并不是一一对应的,一个程序执行在不同的数据集上运行就会成为不同的进程,可以用进程控制块来唯一地标识系统中的每个进程。而这一点正是程序无法做到的,由于程序没有和数据产生直接的联系,既使是执行不同的数据的程序,他们的指令的集合依然是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。一般来说,一个进程肯定有一个与之对应的程序,而且有且只有一个。而一个程序有可能没有与之对应的进程(因为这个程序没有被运行),也有可能有多个进程与之对应(这个程序可能运行在多个不同的数据集上)。

    3. 进程具有并发性而程序没有。

    4. 进程是竞争计算机资源的基本单位,而程序不是。

    进程状态

    在学习进程状态之前,作者决定还是先让读者看看系统中常见的进程状态,可以通过ps命令将系统中运行的进程信息打印出来,我们只需要关注STAT那一列的信息即可,进程的状态非常多种,具体见图37‑5:

    命令:

    ps –ux
    
    # 输出(已删减):
    USER   PID  %CPU  %MEM    VSZ   RSS   TTY      STAT   START    TIME     COMMAND
    xxx    11132   0.0      0.0     15492  5568  pts/1    Ss      00:45    0:00       /bin/bash
    xxx    11340   0.0      0.0     15508  5636  pts/2    Ss+    00:50    0:01       /bin/bash
    xxx    11807   0.0      0.0     14916  4572  pts/3    Ss      01:05    0:00        /bin/bash
    xxx    18319   0.0      0.0     18260   588  pts/1     Ss+     10月09   0:00      bash
    xxx    21862   0.0      0.0      7928   824     ?         S         07:57    0:00      sleep 180
    xxx    26124   0.0      0.0     29580  1540  pts/1     R+      07:58    0:00         ps -ux
    

    由于作者用的是公司服务器,所以只将作者用户当前的进程信息输出,而不是将系统所有进程信息输出,因此ps命令不需要–a选项。

    proces006

    图 37‑5 进程状态

    从图37‑5中可以看到进程的状态有比较多种,有些是S,有些是Ss,还有些是Sl、Rl、R+等状态,具体是什么含义呢?其实是这些状态只是linux系统进程的一部分,还有一些状态是没有显示出来的,因为作者当前用户下的所有进程并没有处于那些状态,所以就没显示出来,下面作者就简单介绍一下linux系统中所有的进程状态,如表格 37‑1所示。

    表格 37‑1 linux系统中进程状态说明

    状态 说明
    R 运行状态。严格来说,应该是“可运行状态”,即表示进程在运行队列中,处于正在执行或即将运行状态,只有在该状态的进程才可能在 CPU 上运行,而同一时刻可能有多个进程处于可运行状态。
    S 可中断的睡眠状态。处于这个状态的进程因为等待某种事件的发生而被挂起,比如进程在等待信号。
    D 不可中断的睡眠状态。通常是在等待输入或输出(I/O)完成,处于这种状态的进程不能响应异步信号。
    T 停止状态。通常是被shell的工作信号控制,或因为它被追踪,进程正处于调试器的控制之下。
    Z 退出状态。进程成为僵尸进程。
    X 退出状态。进程即将被回收。
    s 进程是会话其首进程。
    l 进程是多线程的。
    + 进程属于前台进程组。
    < 高优先级任务。

    进程状态转换

    从前文的介绍我们也知道,进程是动态的活动的实例,这其实指的是进程会有很多种运行状态,一会儿睡眠、一会儿暂停、一会儿又继续执行。虽然Linux操作系统是一个多用户多任务的操作系统,但对于单核的CPU系统来说,在某一时刻,只能有一个进程处于运行状态(此处的运行状态指的是占用CPU),其他进程都处于其他状态,等待系统资源,各任务根据调度算法在这些状态之间不停地切换。但由于CPU处理速率较快,使用户感觉每个进程都是同时运行。

    图 37‑6 展示了Linux进程从被启动到退出的全部状态,以及这些状态发生转换时的条件。

    proces007

    图 37‑6 进程状态转换

    1. 一般来说,一个进程的开始都是从其父进程调用fork()开始的,所以在系统一上电运行的时候,init进程就开始工作,在系统运行过程中,会不断启动新的进程,这些进程要么是由init进程启动的,要么是由被init进程启动的其他进程所启动的。

    2. 一个进程被启动后,都是处于可运行状态(但是此时进程并未占用CPU运行)。处于该状态的进程可以是正在进程等待队列中排队,也可以占用CPU正在运行,我们习惯上称前者为“就绪态”,称后者为“运行态”(占用CPU运行)。

    3. 当系统产生进程调度的时候,处于就绪态的进程可以占用CPU的使用权,此时进程就是处于运行态。但每个进程运行时间都是有限的,比如10毫秒,这段时间被称为“时间片”。当进程的时间片已经耗光了的情况下,如果进程还没有结束运行,那么会被系统重新放入等待队列中等待,此时进程又转变为就绪状态,等待下一次进程的调度。另外,正处于“运行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”,被迫重新回到等到队列中等待。

    4. 处于“运行态”的进程可能会等待某些事件、信号或者资源而进入“可中断睡眠态”,比如进程要读取一个管道文件数据而管道为空,或者进程要获得一个锁资源而当前锁不可获取,甚至是进程自己调用sleep()来强制将自己进入睡眠,这些情况下进程的状态都会变成“可中断睡眠态”。顾名思义,“可中断睡眠态”就是可以被中断的,能响应信号,在特定条件发生后,进程状态就会转变为“就绪态”,比如其他进程想管道文件写入数据后,或者锁资源可以被获取,或者是睡眠时间到达等情况。

    5. 当然,处于“运行态”的进程还可能会进入“不可中断睡眠态”,在这种状态下的进程不能响应信号,但是这种状态非常短暂,读者几乎无法通过ps命令将其显示出来,一般处于这种状态的进程都是在等待输入或输出(I/O)完成,在等待完成后自动进入“就绪态”。

    6. 当进程收到 SIGSTOP 或者 SIGTSTP 中的其中一个信号时,进程状态会被置为“暂停态”,该状态下的进程不再参与调度,但系统资源不会被释放,直到收到SIGCONT信号后被重新置为就绪态。当进程被追踪时(典型情况是使用调试器调试应用程序的情况),收到任何信号状态都会被置为
      TASK_TRACED状态,该状态跟暂停态是一样的,一直要等到 SIGCONT信号后进程才会重新参与系统进程调度。

    7. 进程在完成任务后会退出,那么此时进程状态就变为退出状态,这是正常的退出,比如在main函数内 return 或者调用 exit()函数或者线程调用pthread_exit()都是属于正常退出。为什么作者要强调正常退出呢?因为进程也会有异常退出,比如进程收到kill信号就会被杀死,其实不管怎么死,最后内核都会调用do_exit()函数来使得进程的状态变成“僵尸态(僵尸进程)”,这里的“僵尸”指的是进程的PCB(Process Control Block,进程控制块)。为什么一个进程的死掉之后还要把尸体(PCB)留下呢?因为进程在退出的时候,系统会将其退出信息都保存在进程控制块中,比如如果他正常退出,那进程的退出值是多少呢?如果被信号杀死?那么是哪个信号将其杀死呢?这些“死亡信息”都被一一封存在该进程的PCB当中,好让别人可以清楚地知道:我是怎么死的。那谁会关心他是怎么死的呢?那就是它的父进程,它的父进程之所以要启动它,很大的原因是要让这个进程去干某一件事情,现在这个孩子已死,那事情办得如何,因此需要把这些信息保存在进程控制块中,等着父进程去查看这些信息。

    8. 当父进程去处理僵尸进程的时候,会将这个僵尸进程的状态设置为EXIT_DEAD,即死亡态(退出态),这样子系统才能去回收僵尸进程的内存空间,否则系统将存在越来越多的僵尸进程,最后导致系统内存不足而崩溃。那么还有两个问题,假如父进程由于太忙而没能及时去处理僵尸进程的时候,要怎么处理呢?又假如在子进程变成“僵尸态”之前,它的父进程已经先它而去了(退出),那么这个子进程变成僵死态由谁处理呢?第一种情况可能不同的读者有不同的处理,父进程有别的事情要干,不能随时去处理僵尸进程。在这样的情形下,读者可以考虑使用信号异步通知机制,让一个孩子在变成僵尸的时候,给其父进程发一个信号,父进程接收到这个信号之后,再对其进行处理,在此之前父进程该干嘛就干嘛。而如果如果一个进程的父进程先退出,那么这个子进程将变成“孤儿进程”(没有父进程),那么这个进程将会被他的祖先进程收养(adopt),它的祖先进程是init(该进程是系统第一个运行的进程,他的 PCB是从内核的启动镜像文件中直接加载的,系统中的所有其他进程都是init进程的后代)。那么当子进程退出的时候,init进程将回收这些资源。

    启动新进程

    在linux中启动一个进程有多种方法,比如可以使用system()函数,也可以使用fork()函数去启动(在其他的一些linux书籍也称作创建进程,本书将全部称之为启动进程)一个新的进程,第一种方法相对简单,但是在使用之前应慎重考虑,因为它效率低下,而且具有不容忽视的安全风险。第二种方法相对复杂了很多,但是提供了更好的弹性、效率和安全性。

    system()

    这个system ()函数是C标准库中提供的,它主要是提供了一种调用其它程序的简单方法。读者可以利用system()函数调用一些应用程序,它产生的结果与从 shell中执行这个程序基本相似。事实上,system()启动了一个运行着/bin/sh的子进程,然后将命令交由它执行。

    我们举个例子,在野火提供的application/system目录下,找到system.c文件,它里面的应用例程就是使用system()函数启动一个新进程ls,具体的代码如代码清单37‑1所示:

    代码清单 37‑1 system.c文件源码

    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        pid_t result;
    
        printf("This is a system demo!\n\n");
    
        /*调用 system()函数*/
        result = system("ls -l");
    
        printf("Done!\n\n");
    
        return result;
    }
    

    在代码的第13行,就调用了这个system()函数,并且传入了一个命令“ls -l”这个命令与在shell中运行的结果是一样的,调用 system()函数的返回值就是被调用的 shell 命令的返回值。如果系统中 shell自身无法运行,system() 函数返回 127;如果出现了其它错误, system()函数将返回-1,为了简单,作者在这个例子中并没有检查system调用是否能够真正的工作。因为system() 函数使用 shell 调用命令,它受到系统 shell自身的功能特性和安全缺陷的限制,因此,作者并不推荐使用这种方法去启动一个进程。

    我们可以尝试编译它,在application/system目录下还会提供对应编译的Makefile文件,这是一个通用的Makefile文件,所有application的例程都使用这个Makefile文件编译,具体见代码清单
    37‑2。

    代码清单 37‑2 Makefile源码

    CC = gcc
    SRC = $(wildcard *.c */*.c)
    OBJS = $(patsubst %.c, %.o, $(SRC))
    DEP_FILES := $(patsubst %, .%.d,$(OBJS))
    DEP_FILES := $(wildcard $(DEP_FILES))
    FLAG = -g -Werror -I. -Iinclude 
    TARGET = targets
    
    $(TARGET):$(OBJS)
    	$(CC) -o $@ $^ $(FLAG)
    
    ifneq ($(DEP_FILES),)
    include $(DEP_FILES)
    endif
    
    %.o:%.c
    	$(CC) -o $@ -c $(FLAG) $< -g -MD -MF .$@.d
    
    clean:
    	rm -rf $(TARGET) $(OBJS)
    
    distclean:
    	rm -rf $(DEP_FILES)
    
    .PHONY:clean
    

    解释一下Makefile文件中的代码含义:

    • 第3行指定编译器为 gcc,可以根据需要修改为 g++ 或者 arm-linux-gcc等交叉编译工具链,使用CC变量保存。

    • 第4行是为了获取匹配模式的文件名,*.c 表示当前工程目录的 c文件,*/*.c表示所有下一级目录的 .c文件,这些文件名保存在SRC变量中。

    • 第5行是将 $(SRC) 中的 .c 文件都替换成对应的目标 .o文件,并且保存在OBJS变量中。

    • 第6 - 7行将根据是有的目标文件替换成 .o.d文件(隐藏的依赖文件),并且通过DEP_FILES变量保存。

    • 第8行用于指定编译选项并且保存在FLAG变量中,读者根据需要添加,比如-g、-ml、-Wall、-O2等等,在这里作者提个小建议,编译选项最后选上-Werror,这个选项的含义是存在警告就会报错,它会使我们的代码更加严谨。

    • 第9行指定最终生成的可执行文件名为targets

    • 第11行的$(TARGET):$(OBJS)表示由 .o 文件链接成可执行文件。

    • 注意第12行前面是一个 <tab> 键,而 $@ 表示目标,也就是$(TARGET),$^ 表示依赖目标,也就是 $(OBJS)
      ,编译选项则是$(FLAG)

    • 第14 - 16行则是判断,判断依赖文件是否存在,如果不存在则需要包含DEP_FILES变量。

    • 第18行表示将所有的.c文件编译编译成.o文件 。

    • 第19行的开头也是一个<tab>键,$< 表示搜索到的第一个匹配的文件,而接下来的-g -MD -MF则是编译器的语法,-g表示以操作系统的本地格式产生调试信息,GDB能够使用这些调试信息进行调试; -MD -MF则表示生成文件的依赖关系,同时也把一些标准库的头文件包含了进来。本质是告诉预处理器输出一个适合 make 的规则,用于描述各目标文件的依赖关系。

    • 第21 – 25行表示清除相关的依赖文件,目标文件等。

    • .PHONY表示clean是个伪目标文件。

    进入application/system目录下,运行make命令将system.c编译,然后可以看到application/system目录下多了一个可执行文件——target,然后运行这个文件,可以看到调用system()函数启动一个进程输出的结果,它与我们在shell终端中执行ls –l命令产生的结果是一致的,具体见图 37‑7。

    命令:

    make
    
    # 输出:
    
    gcc -o system.o -c -g -Werror -I. -Iinclude system.c -g -MD -MF
    .system.o.dgcc -o targets system.o -g -Werror -I. –Iinclude
    
    

    ps:此时已生成target可执行文件

    proces008

    图 37‑7 system()函数运行结果与ls命令运行结果

    从程序运行的结果可以看到,只有当system()函数运行完毕之后,才会输出Done,这是因为程序从上往下执行,而无法直接返回结果。虽然system()函数很有用,但它也有局限性,因为程序必须等待由system()函数启动的进程结束之后才能继续,因此我们不能立刻执行其他任务。

    当然,你也可以让“ls -l”命令在后台运行,只需在命令结束位置加上“&”即可,具体命令如下:

    命令:

    ls –l &
    

    如果在system()函数中使用这个命令,它也是可以在后台中运行的,那么system()函数的调用将在shell命令结束后立刻返回。由于它是一个在后台运行程序的请求,所以ps程序一启动shell就返回了,代码如代码清单37‑3所示。

    代码清单 37‑3 修改system.c源码:

    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        pid_t result;
    
        printf("This is a system demo!\n\n");
    
        /*调用 system()函数*/
        result = system("ls -l &");
    
        printf("Done!\n\n");
    
        return result;
    }
    

    重新执行make编译,然后运行程序,实验现象具体见图 37‑8。

    proces009

    图 37‑8 system后台运行

    从图 37‑8就可以看出来,在ls命令还未来得及打印出它的所有输出结果之前,system()函数就程序就打印出字符串Done然后退出了。在system()程序退出后,ls命令继续完成它的输出。这类的处理行为往往会给用户带来很大的困惑,也不一定如用户所预料的结果一致,因此如果读者想要让进程按照自己的意愿执行,就需要能够对它们的行为做更细致的控制,接下来作者就会讲解其他方式启动新的进程。

    fork()

    在前面的文章我们也了解到,init进程可以启动一个子进程,它通过fork()函数从原程序中创建一个完全分离的子进程,当然,这只是init进程启动子进程的第一步,后续还有其他操作的。不管怎么说,fork()函数就是可以启动一个子进程,其示意图具体见图37‑9。

    在父进程中的fork()调用后返回的是新的子进程的PID。新进程将继续执行,就像原进程一样,不同之处在于,子进程中的fork()函数调用后返回的是0,父子进程可以通过返回的值来判断究竟谁是父进程,谁是子进程。

    proces010

    图 37‑9 fork()示意图

    fork()函数用于从一个已存在的进程中启动一个新进程,新进程称为子进程,而原进程称为父进程。使用fork()函数的本质是将父进程的内容复制一份,正如细胞分裂一样,得到的是几乎两个完全一样的细胞,因此这个启动的子进程基本上是父进程的一个复制品,但子进程与父进程有不一样的地方,作者就简单列举一下它们的联系与区别。

    子进程与父进程一致的内容:

    • 进程的地址空间。
    • 进程上下文、代码段。
    • 进程堆空间、栈空间,内存信息。
    • 进程的环境变量。
    • 标准 IO 的缓冲区。
    • 打开的文件描述符。
    • 信号响应函数。
    • 当前工作路径。

    子进程独有的内容:

    • 进程号 PID。 PID 是身份证号码,是进程的唯一标识符。
    • 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
    • 挂起的信号。这些信号是已经响应但尚未处理的信号,也就是“悬挂”的信号,子进程也不会继承这些信号。

    因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序,但是这种复制有一个很大的问题,那就是资源与时间都会消耗很大,当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,因为它需要做一些事情:

    • 为子进程的页表分配页面。
    • 为子进程的页分配页面。
    • 初始化子进程的页表。
    • 把父进程的页复制到子进程相应的页中

    创建一个地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容,因此直接复制物理内存对系统的开销会产生很大的影响,更重要的是在大多数情况下,这样直接拷贝通常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。因此在linux中引入一种写时复制技术(Copy On Write,简称COW),我们知道,linux系统中的进程都是使用虚拟内存地址,虚拟地址与真实物理地址之间是有一个对应关系的,每个进程都有自己的虚拟地址空间,而操作虚拟地址明显比直接操作物理内存更加简便快捷,那么显而易见的,写时复制是一种可以推迟甚至避免复制数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间(页面)。

    那么写时复制的思想就是在于:父进程和子进程共享页面而不是复制页面。而共享页面就不能被修改,无论父进程和子进程何时试图向一个共享的页面写入内容时,都会产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的,当还有进程试图写入时,内核检查写进程是否是这个页面的唯一属主,如果是则把这个页面标记为对这个进程是可写的。

    总的来说,写时复制只会用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间,资源的复制是在需要写入的时候才会进行,在此之前,父进程与子进程都是以只读方式共享页面,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。而在绝大多数的时候共享的页面根本不会被写入,例如,在调用fork()函数后立即执行exec(),地址空间就无需被复制了,这样一来fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。

    理论相关的知识就讲解到这里就好了,作者也不打算再深入讲解,下面就看看fork()函数的使用,它的函数原型如下:

    pid_t fork(void);
    

    在fork()启动新的进程后,子进程与父进程开始并发执行,谁先执行由内核调度算法来决定。fork()函数如果成功启动了进程,会对父子进程各返回一次,其中对父进程返回子进程的
    PID,对子进程返回0;如果fork()函数启动子进程失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。

    在野火提供的application/fork目录下,找到fork.c文件,它里面的应用例程就是使用fork()函数启动一个新进程,并且在进程中打印相关的信息,如在父进程中打印出“In father process!!”等信息,例程源码具体见代码清单 37‑4。

    代码清单 37‑4 fork.c源码

    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    int main(void)
    {
        pid_t result;
    
        printf("This is a fork demo!\n\n");
    
        /*调用 fork()函数*/
        result = fork();
    
        /*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
        if(result == -1) {
            printf("Fork error\n");
        }
    
        /*返回值为 0 代表子进程*/
        else if (result == 0) {
            printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
    
        }
    
        /*返回值大于 0 代表父进程*/
        else {
            printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
        }
    
        return result;
    }
    

    我们来分析一下这段代码:

    • 首先在第12行的时候调用了fork函数,调用fork函数后系统就会启动一个子进程,并且子进程与父进程执行的内容是一样的(代码段),读者可以通过返回值result判断fork()函数的执行结果。
    • 如果result的值为-1,那代表着fork()函数执行出错,出错的原因在前文也提到,在此具体不细说。
    • 如果返回的值为0,则表示此时执行的代码是子进程,那么就打印返回的结果、“In child process!!”与子进程的PID,进程的PID通过getpid()函数获取得到。
    • 如果返回的值大于0,则表示此时执行的代码是父进程,同样也打印出返回的结果、“In father process!!”与父进程的PID。

    在application/fork目录下也提供了对应的Makefile文件,可以直接运行make进行编译,然后执行编译后生成的可执行文件“targets”,现象具体见图37‑10。

    proces011

    图 37‑10 fork实验现象

    细心的同学就会发现,在这个实验现象中,父进程的返回值就是子进程的PID,而子进程的返回值则是0。

    exce系列函数

    事实上,使用fork()函数启动一个子进程是并没有太大作用的,因为子进程跟父进程都是一样的,子进程能干的活父进程也一样能干,因此世界各地的开发者就想方设法让子进程做不一样的事情,因此就诞生了exce系列函数,这个系列函数主要是用于替换进程的执行程序,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。简单来说就是覆盖进程,举个例子,A进程调用exce系列函数启动一个进程B,此时进程B会替换进程A,进程A的内存空间、数据段、代码段等内容都将被进程B占用,进程A将不复存在。

    exec 族函数有 6 个不同的 exec 函数,函数原型分别如下:

     int execl(const char *path, const char *arg, ...)
    
     int execlp(const char *file, const char *arg, ...)
    
     int execle(const char *path, const char *arg, ..., char *const envp[])
    
     int execv(const char *path, char *const argv[])
    
     int execvp(const char *file, char *const argv[])
    
     int execve(const char *path, char *const argv[], char *const envp[])
    

    这些函数可以分为两大类, execl、 execlp和execle的参数个数是可变的。execv、execvp和execve的第2个参数是一个字符串数组,参数以一个空指针NULL结束,无论何种函数,在调用的时候都会通过参数将这些内容传递进去,传入的参数一般都是要运行的程序(可执行文件)、脚本等。

    总结来说,可以通过它们的后缀来区分他们的作用:

    • 名称包含 l 字母的函数(execl、 execlp 和execle)接收参数列表“list”作为调用程序的参数。

    • 名称包含 p 字母的函数(execvp 和execlp)接受一个程序名作为参数,然后在当前的执行路径中搜索并执行这个程序;名字不包含p字母的函数在调用时必须指定程序的完整路径,其实就是在系统环境变量“PATH”搜索可执行文件。

    • 名称包含 v 字母的函数(execv、execvp 和 execve)的命令参数通过一个数组“vector”传入。

    • 名称包含 e 字母的函数(execve 和 execle)比其它函数多接收一个指明环境变量列表的参数,并且可以通过参数envp传递字符串数组作为新程序的环境变量,这个envp参数的格式应为一个以 NULL 指针作为结束标记的字符串数组,每个字符串应该表示为“environment =
      : virables”的形式。

    下面作者就具体某个函数做介绍:

    函数:

    int execl(const char *path, const char *arg, ...)
    

    execl()函数用于执行参数path字符串所代表的文件路径(必须指定路径),接下来是一系列可变参数,它们代表执行该文件时传递过去的argv[0]、argv[1]… argv[n],最后一个参数必须用空指针NULL作为结束的标志。

    代码清单 37‑5 execl()函数实例

    int main(void)
    {
        int err;
    
        printf("this is a execl function test demo!\n\n");
    
        err = execl("/bin/ls", "ls", "-la", NULL);
    
        if (err < 0) {
            printf("execl fail!\n\n");
        }
        
        printf("Done!\n\n");
    }
    

    如以上的execlp()函数实例代码,它其实就是与我们在终端上运行“ls
    -la”产生的结果是一样的。

    函数:

    int execlp(const char *file, const char *arg, ...)
    

    execlp()函数会从PATH环境变量所指的目录中查找符合参数file的文件名(不需要指定路径),找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]… argv[n], 最后一个参数必须用空指针NULL作为结束的标志。

    代码清单 37‑6 execlp()函数实例

    int main(void)
    {
        int err;
    
        printf("this is a execlp function test demo!\n\n");
    
        err = execlp("ls", "ls", "-la", NULL);
    
        if (err < 0) {
            printf("execlp fail!\n\n");
        }
    }
    

    函数:

    int execle(const char *path, const char *arg, ..., char *const envp[])
    

    execle()函数用于执行参数path字符串所代表的文件路径(必须指定路径),并为新程序复制最后一个参数所指示的环境变量(envp)。

    代码清单 37‑7 execle()函数实例

    int main(void)
    {
        int err;
        char *envp[] = {
            "/bin", NULL
        };
    
        printf("this is a execle function test demo!\n\n");
    
        err = execle("/bin/ls", "ls", "-la", NULL, envp);
    
        if (err < 0) {
            printf("execle fail!\n\n");
        }
    }
    

    函数:

    int execv(const char *path, char *const argv[])
    

    execv()函数用于执行参数path字符串所代表的文件路径(必须指定路径),接着传入一个数组作为执行该文件时传递过去的参数argv[0]、argv[1]… argv[n],以空指针NULL结束。

    代码清单 37‑8 execv()函数实例

    int main(void)
    {
        int err;
        char *argv[] = {
            "ls", "-la", NULL
        };
    
        printf("this is a execv function test demo!\n\n");
    
        err = execv("/bin/ls", argv);
    
        if (err < 0) {
            printf("execv fail!\n\n");
        }
    }
    

    函数

    int execvp(const char *path, char *const argv[])
    

    execvp()函数会从PATH环境变量所指的目录中查找符合参数file的文件名(不需要指定路径),找到该文件后便执行该文件,接着传入一个数组作为执行该文件时传递过去的参数argv[0]、argv[1] … argv[n],以空指针NULL结束。

    代码清单 37‑9 execvp()函数实例

    int main(void)
    {
        int err;
        char *argv[] = {
            "ls", "-la", NULL
        };
    
        printf("this is a execvp function test demo!\n\n");
    
        err = execvp("ls", argv);
    
        if (err < 0) {
            printf("execvp fail!\n\n");
        }
    }
    

    函数:

    int execve(const char *path, char *const argv[], char *const envp[])
    

    execve()函数用于执行参数path字符串所代表的文件路径(必须指定路径),执行该文件时会传入一个数组作为执行该文件时传递过去的参数argv[0]、argv[1] … argv[n],除此之外该函数还会为新程序复制最后一个参数所指示的环境变量(envp)。

    代码清单 37‑10 execve ()函数实例

    int main(void)
    {
        int err;
        char *argv[] = {
            "ls", "-la", NULL
        };
        char *envp[] = {
            "/bin", NULL
        };
    
        printf("this is a execve function test demo!\n\n");
    
        err = execve("/bin/ls", argv, envp);
    
        if (err < 0) {
            printf("execve fail!\n\n");
        }
    }
    

    以上函数实例代码均在application/exce目录下,选择对应的代码进行编译即可,该目录也提供了对应的Makefile文件,可以直接运行make进行编译,然后执行编译后生成的可执行文件“targets”,具体现象如图
    37‑11所示。

    proces012

    图 37‑11 exce系列函数实验现象

    程序先打印出它的第一条消息“this is a execl function test demo!”,接着调用exec系列函数(实验中使用execl()函数),这个函数在/bin/ls目录中搜索程序ls,然后用这个程序替换targets程序,这与直接在终端中使用以下所示的shell命令一样,如图37‑12所示。

    命令:

    ls -la
    

    proces013

    图 37‑12 ls –la命令

    注意,exce系列函数是直接将当前进程给替换掉的,当调用exce系列函数后,当前进程将不会再继续执行,我们可以测试一下,在调用exce系列函数后再打印一句话,具体代码如代码清单37‑11加粗部分所示。

    代码清单 37‑11 exce系列函数测试代码

    int main(void)
    {
        int err;
    
        printf("this is a execl function test demo!\n\n");
    
        err = execl("/bin/ls", "ls", "-la", NULL);
    
        if (err < 0) {
            printf("execl fail!\n\n");
        }
        
        printf("Done!\n\n");
    }
    

    在程序运行后,“Done!”将不被输出,因为当前进程已经被替换了,一般情况下,
    exec系列函数函数是不会返回的,除非发生了错误。出现错误时,
    exec系列函数将返回-1,并且会设置错误变量errno。

    因此我们可以通过调用fork()复制启动一个子进程,并且在子进程中调用exec系列函数替换子进程,这样子
    fork()和exec系列函数结合在一起使用就是创建一个新进程所需要的一切了。

    终止进程

    在linux系统中,进程终止(或者称为进程退出,为了统一,下文均使用“终止”一词)的常见方式有5种,可以分为正常终止与异常终止:

    正常终止:

    • 从main函数返回。

    • 调用exit()函数终止。

    • 调用_exit()函数终止。

    异常终止:

    • 调用abort()函数异常终止。

    • 由系统信号终止。

    在linux系统中,exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中,exit()和_exit()函数都是用来终止进程的,当程序执行到exit()或_exit()函数时,进程会无条件地停止剩下的所有操作,清除包括 PCB在内的各种数据结构,并终止当前进程的运行。不过这两个函数还是有区别的,具体如图37‑13所示。

    proces014

    图 37‑13 exit()和_exit()函数的区别

    从图中可以看出,_exit()函数的作用最为简单:直接通过系统调用使进程终止运行,当然,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;而exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序:比如exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除I/O缓冲”。

    由于在 Linux 的标准函数库中,有一种被称作“缓冲 I/O(buffered I/O)”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用 exit()函数。

    不管是那种退出方式,系统最终都会执行内核中的同一代码,这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源。

    下面一起看看_exit()与exit()函数的使用方法:

    头文件:

    #include <unistd.h>
    #include <stdlib.h>
    

    函数原型:

    void _exit(int status);
    void exit(int status);
    

    这两个函数都会传入一个参数status,这个参数表示的是进程终止时的状态码,0表示正常终止,其他非0值表示异常终止,一般都可以使用-1或者1表示,标准C里有EXIT_SUCCESS和EXIT_FAILURE两个宏,表示正常与异常终止。

    这些函数的使用都是非常简单的,只需要在需要终止的地方调用一下即可,此处就不深入讲解。

    等待进程

    在linux中,当我们使用fork()函数启动一个子进程时,子进程就有了它自己的生命周期并将独立运行,在某些时候,可能父进程希望知道一个子进程何时结束,或者想要知道子进程结束的状态,甚至是等待着子进程结束,那么我们可以通过在父进程中调用wait()或者waitpid()函数让父进程等待子进程的结束。

    从前面的文章我们也了解到,当一个进程调用了exit()之后,该进程并不会立刻完全消失,而是变成了一个僵尸进程。僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。那么无论如何,父进程都要回收这个僵尸进程,因此调用wait()或者waitpid()函数其实就是将这些僵尸进程回收,释放僵尸进程占有的内存空间,并且了解一下进程终止的状态信息。

    我们可以在终端中通过man命令查看关于wait相关的函数,具体命令如下:

    命令:

    man 2 wait
    
    # 输出
    
    NAME
           wait, waitpid, waitid - wait for process to change state
    
    SYNOPSIS
           #include <sys/types.h>
           #include <sys/wait.h>
    
           pid_t wait(int *wstatus);
    
           pid_t waitpid(pid_t pid, int *wstatus, int options);
    
           int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    # ……(省略其他)
    

    可能很多读者对man命令不了解,那我就简单说一下man命令相关的内容吧,其实在linux系统中是提供了丰富的帮助手册,当你需要查看某个命令的参数时不必到处上网查找,只要man一下即可,man命令是就是用于找出这些帮助手册的内容的,比如有什么shell命令,有什么可以调用的函数
    等等。

    man 命令是按照章节存储的,linux的man手册共有8个章节,具体见表格 37‑2。

    表格 37‑2 man 命令说明:

    章节编号 章节名称 章节主要内容
    1 General Commands 用户在shell中可以操作的指令或者可执行文档
    2 System Calls 系统调用的函数与工具等
    3 Sunroutines C语言库函数
    4 Special Files 设备或者特殊文件
    5 File Formats 文件格式与规则
    6 Games 游戏及其他
    7 Macros and Conventions 表示宏、包及其他杂项
    8 Maintenence Commands 表示系统管理员相关的命令

    例如我们想找与wait相关的函数,那么我们只需要输入以下命令即可:

    man 2 wait
    

    例如我们想要了解ls命令相关的内容,我们可以直接输入以下命令,就可以看到关于ls相关的用法等内容。

    命令:

    man ls
    
    # 输出
    
    NAME
           ls - list directory contents
    
    SYNOPSIS
           ls [OPTION]... [FILE]...
    
    DESCRIPTION
           List information about the FILEs (the current directory by default).  Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
    
           Mandatory arguments to long options are mandatory for short options too.
    
           -a, --all
                  do not ignore entries starting with .
    
    ……(省略其他)
    

    当然啦,man手册是英文的,这是属于全世界通用的技术交流语言,因此读者还是需要对英文有一定熟悉程度。

    wait()

    我们通过man命令就知道了wait()、waitpid()函数原型,那么我们就首先了解下wait()函数。

    函数原型

    pid_t wait(int *wstatus);
    

    wait()函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束,如果在调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。子进程的结束状态信息会由参数wstatus返回,与此同时该函数会返子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,如果不在意子进程的结束状态信息,则参数wstatus可以设成NULL。

    wait()函数有几点需要注意的地方:

    1. wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID。
    2. 参数wstatus用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针,但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样做),我们就可以设定这个参数为NULL。

    当然,除此之外,linux系统中还提供关于等待子进程退出的一些宏定义,我们可以使用这些宏定义来直接判断子进程退出的状态:

    • WIFEXITED(status) :如果子进程正常结束,返回一个非零值

    • WEXITSTATUS(status): 如果WIFEXITED非零,返回子进程退出码

    • WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值

    • WTERMSIG(status) :如果WIFSIGNALED非零,返回信号代码

    • WIFSTOPPED(status): 如果子进程被暂停,返回一个非零值

    • WSTOPSIG(status): 如果WIFSTOPPED非零,返回一个信号代码

    wait()函数使用实例如下:

    代码清单 37‑12 wait()函数使用实例

    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        pid_t pid, child_pid;
        int status;
    
        pid = fork();                  //(1)
    
        if (pid < 0) {
            printf("Error fork\n");
        }
        /*子进程*/
        else if (pid == 0) {                  //(2)
    
            printf("I am a child process!, my pid is %d!\n\n",getpid());
    
            /*子进程暂停 3s*/
            sleep(3);
    
            printf("I am about to quit the process!\n\n");
    
            /*子进程正常退出*/
            exit(0);                          //(3)
        }
        /*父进程*/
        else {                                //(4)
    
            /*调用 wait,父进程阻塞*/
            child_pid = wait(&status);        //(5)
    
            /*若发现子进程退出,打印出相应情况*/
            if (child_pid == pid) {
                printf("Get exit child process id: %d\n",child_pid);
                printf("Get child exit status: %d\n\n",status);
            } else {
                printf("Some error occured.\n\n");
            }
    
            exit(0);
        }
    }
    

    我们来分析一下这段代码:

    代码清单 37‑12 (1):首先调用fork()函数启动一个子进程。

    代码清单 37‑12 (2):如果fork()函数返回的值pid为0,则表示此时运行的是子进程,那么就让子进程输出一段信息,并且休眠3s。

    代码清单37‑12 (3):休眠结束后调用exit()函数退出,退出状态为0,表示子进程正常退出。

    代码清单 37‑12 (4):如果fork()函数返回的值pid不为0,则表示此时运行的是父进程,那么在父进程中调用wait(&status)函数等待子进程的退出,子进程的退出状态将保存在status变量中。

    代码清单37‑12 (5):若发现子进程退出(通过wait()函数返回的子进程pid判断),则打印出相应信息,如子进程的pid与status。

    以上函数实例代码在application/wait目录下,选择对应的代码进行编译即可,该目录也提供了对应的Makefile文件,可以直接运行make进行编译,然后执行编译后生成的可执行文件“targets”,执行结果如图
    37‑14所示。

    proces015

    图 37‑14 wait()函数现象

    waitpid()

    waitpid()函数 的作用和wait()函数一样,但它并不一定要等待第一个终止的子进程,它还有其他选项,比如指定等待某个pid的子进程、提供一个非阻塞版本的wait()功能等。实际上 wait()函数只是 waitpid() 函数的一个特例,在 linux内部实现 wait 函数时直接调用的就是 waitpid 函数。

    函数原型

    pid_t waitpid(pid_t pid, int *wstatus, int options);
    

    waitpid()函数的参数有3个,下面就简单介绍这些参数相关的选项:

    • pid:参数pid为要等待的子进程ID,其具体含义如下:
    1. pid < -1:等待进程组号为pid绝对值的任何子进程。
    2. pid = -1:等待任何子进程,此时的waitpid()函数就等同于wait()函数。
    3. pid =0:等待进程组号与目前进程相同的任何子进程,即等待任何与调用waitpid()函数的进程在同一个进程组的进程。
    4. pid > 0:等待指定进程号为pid的子进程。
    • wstatus:与wait()函数一样。
    • options:参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。
    1. WNOHANG:如果pid指定的子进程没有终止运行,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号与状态信息。
    2. WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则马上返回。
    3. WCONTINUED:如果子进程恢复通过SIGCONT信号运行,也会立即返回(这个不常用,了解一下即可)。

    很显然,当waitpid()函数的参数为(-1, status, 0)时,waitpid()函数就完全退化成了wait()函数。

    下面看一下waitpid()函数使用实例,具体见代码清单 37‑13。

    代码清单 37‑13 waitpid()函数使用实例

    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    
    int main()
    {
        pid_t pid, child_pid;
        int status;
    
        pid = fork();
    
        if (pid < 0) {
            printf("Error fork\n");
        }
        /*子进程*/
        else if (pid == 0) {
    
            printf("I am a child process!, my pid is %d!\n\n",getpid());
    
            /*子进程暂停 3s*/
            sleep(3);
    
            printf("I am about to quit the process!\n\n");
            /*子进程正常退出*/
            exit(0);
        }
        /*父进程*/
        else {
    
            /*调用 waitpid,且父进程不阻塞*/
            child_pid = waitpid(pid, &status, WUNTRACED);
    
            /*若发现子进程退出,打印出相应情况*/
            if (child_pid == pid) {
                printf("Get exit child process id: %d\n",child_pid);
                printf("Get child exit status: %d\n\n",status);
            } else {
                printf("Some error occured.\n");
            }
    
            exit(0);
        }
    }
    

    编译后运行,它的实验现象与wait()函数的是一样的。

    展开全文
  • 进程管理是操作系统重点、难点问题,也是贯穿Linux学习的知识点。那么什么是进程? 为什么引入进程的概念?  从理论角度看,是对正在运行的程序过程的抽象;  从实现角度看,是一种数据结构,目的在于清晰...

    进程管理是操作系统重点、难点问题,也是贯穿Linux学习的知识点。那么什么是进程?

    • 为什么引入进程的概念?

       从理论角度看,是对正在运行的程序过程的抽象;
       从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

    • 什么是进程?

       狭义定义:进程就是一段程序的执行过程。
       广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

    • 进程有怎么样的特征?

      动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
      并发性:任何进程都可以同其他进程一起并发执行
      独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
      异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推 进
      结构特征:进程由程序、数据和进程控制块三部分组成;
      多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果; 但是执行过程中,程序不能发生改变。

    • Linux进程结构?

          Linux进程结构:可由三部分组成:代码段、数据段、堆栈段。也就是程序、数据、进程控制块PCB(Process Control Block)组成。进程控制块是进程存在的惟一标识,系统通过PCB的存在而感知进程的存在。
          系统通过PCB对进程进行管理和调度。PCB包括创建进程、执行程序、退出进程以及改变进程的优先级等。而进程中的PCB用一个名为task_struct的结构体来表示,定义在include/linux/sched.h中,每当创建一新进程时,便在内存中申请一个空的task_struct结构,填入所需信息,同时,指向该结构的指针也被加入到task数组中,所有进程控制块都存储在task[]数组中。

    • 进程的三种基本状态?

         a> 就绪状态:进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
         b> 运行状态:进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以 执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
         c> 阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生 前即使把处理机分配给该进程,也无法运行。

    • 进程和程序的区别?

         a> 程序是指令和数据的有序集合,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个 动态的概念。
         b> 程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
          c> 进程是由进程控制块、程序段、数据段三部分组成;
          d> 进程具有创建其他进程的功能,而程序没有。
          e> 同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进 程。
          f> 在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。

    • 进程控制?

      创建进程
      引起创建进程的事件:
      1) 用户登录
      2) 作业调度
      3) 提供服务
      4) 应用请求
      进程的创建过程
      一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语Creat()按下述步骤创建一个新 进程。
      1) 申请空白PCB。为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
      2) 为新进程分配资源。
      3) 初始化进程控制块。PCB的初始化包括:
      ①初始化标识信息,将系统分配的标识符和父进程标识符,填入新的PCB中。
      ②初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶。
      ③初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常 是将它设置为最低优先级,除非用户以显式的方式提出高优先级要求。
      4) 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列中
      进程终止
      引起进程终止的事件
      1)正常结束
      2)异常结束
      3)外界干预
      进程的终止过程
      如果系统发生了上述要求终止进程的某事件后,OS便调用进程终止原语,按下述过程去终止指定的进 程。
      1)根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程状态。
      2)若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真。用于指示该进程 被终止后应重新进行调度。
      3)若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防他们成为不可控的进程。
      4)将被终止的进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
      5)将被终止进程(它的PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息。
      阻塞唤醒
      1.引起进程阻塞和唤醒的事件
      1)请求系统服务
      2)启动某种操作
      3)新数据尚未到达
      4)无新工作可做
      2.进程阻塞过程
      正在执行的进程,当发现上述某事件后,由于无法继续执行,于是进程便通过调用阻塞原语block把自 己阻塞。可见,进程的阻塞是进程自身的一种主动行为。进入block过程后,由于此时该进程还处于执 行状态,所以应先立即停止执行,把进程控制块中的现行状态由执行改为阻塞,并将PCB插入阻塞队 列。如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞 (等待)队列。最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,亦即, 保留被阻塞进程的处理机状态(在PCB中),再按新进程的PCB中的处理机状态设置CPU环境。
      3.进程唤醒过程
      当被阻塞的进程所期待的事件出现时,如I/O完成或者其所期待的数据已经到达,则由有关进程(比如, 用完并释放了该I/O设备的进程)调用唤醒原语wakeup(),将等待该事件的进程唤醒。唤醒原语执行 的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就 绪,然后再将该PCB插入到就绪队列中。

    展开全文
  • 进程:是系统进行资源分配和调度的一个独立单位。1、独立性,进程是系统中独立存在的实体,它可以拥有自己的独立资源,每一个进程都有自己的私有地址空间。在没有进过进程本身允许的情况下,一个用户进程不可以直接...
  • 操作系统fork()进程

    2019-03-01 11:57:11
    1、fork()是创建进程函数。 2、c程序一开始,就会产生 一个进程,当这个进程执行到fork()的时候,会创建一个子进程。 3、此时父进程和子进程是共存的,它们俩会一起向下执行c程序的代码。 4、需要注意!!!子...
  • 一、进程 1.1 多道程序设计 允许多个程序同时进入内存并运行,提高CPU的利用率,目的是提高系统效率 a图内存中有四个程序,串行执行,因为这里只有一个程序计数器。 当有了多道程序技术之后就得到...
  • 进程及多进程编程

    2018-08-17 15:05:56
    第一部分 进程知识总结  一、进程的定义 进程是执行中的程序,就类比于一出舞台剧的整个表演过程;进程动态性的实质是进程实体的执行过程;进程独立性是指每个进程都有自己的PCB;进程的并发性是内存中可以允许...
  • 进程切换 一开始我并不想写这个笔记,因为太过复杂,我一直想以简单的方式理解内核,只从概念,避免涉及过多的代码。实际上,我写笔记的时候,书已经看到很后面了,因为总要理解更多才能理解之前看似简短实际复杂的...
  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己...
  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类...
  • 下面将讲解进程间通信的另一种方式,使用共享内存。 一、什么是共享内存 顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式...
  • Tomcat进程意外退出的问题分析
  • 正常情况下,每一个Android应用启动后都会对应一个进程,当前越来越多应用会有多个进程,为了推送,为了内存,或者为了保活。如何查看应用进程呢。1.DOS下面cmd,然后打开adb shell,直接ps命令,显示当前手机所有...
  • 长期以来,Linux一直把具有较好的平均系统响应时间和较高的吞吐量作为调度算法的主要目标。...为使每个进程都有运行的机会,调度器为每个进程分配了一个占用处理器的时间额度,这个额度叫做进程的“时间片”,...
  • 虽然内核对象位于独立于进程之外的内核区域,我们在开发中却只能通过调用Win32 API传入HANDLE参数来操作内核对象(如SetEvent等)。然而HANDLE句柄只对当前进程有效,离开了当前进程该句柄就无效了(具体原因参考:...
  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。 但是仍然为其保留一定的信息,这些信息主要主要指进程控制块的信息(包括进程号、退出状态、运行时间等)。直到父进程通过 wait()...
  • 守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与...
  • 进程和线程的区别

    2016-06-27 20:53:51
    操作系统面试题 1、进程和线程的区别? 解析:(1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本... (3)进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的
1 2 3 4 5 ... 20
收藏数 2,158,976
精华内容 863,590
关键字:

进程