3.1 进程概念

1. 进程

进程是一种执行中的程序

  • 执行什么程序

  • 执行什么数据

  • 处在什么状态

进程包括

  • 程序代码/文本段

  • 当前活动,程序计数器和CPU寄存器

  • 内存中的进程

    • 堆栈段(临时数据,如函数参数,返回地址,局部变量)

    • 数据段(如全局变量)

    • 堆(进程运行期间动态分配的内存)

内存中的进程

进程 VS 程序

  • 程序是被动实体,是可执行代码/指令文件内容

  • 进程是活动实体,当一个程序被装载入内存(执行)时,程序变成进程

2. 进程状态

  • 新的:进程正在被创建

  • 运行:指令正在被执行

  • 等待:进程等待某个事件的发生(如I/O完成或受到信号)

  • 就绪 :进程等待分配处理器

  • 终止 :进程完成执行

进程状态

3. 进程控制块 (process control block, PCB)

  • 标识符:id

  • 进程状态: 状态可包括新的,就绪,运行,等待,终止等

  • 程序计数器 : 计数器表示进程要执行的下个指令的地址

  • CPU寄存器: 与程序计数器一起,这些寄存器的状态信息在出现中断时也需要保存,以便进程以后能正确的执行

  • CPU调度信息:这类信息包括进程优先级、调度队列指针和其他调度参数

  • 内存管理信息:根据内存系统,这类信息包括基址和界限寄存器的值,页表或段表

  • 记账信息:包括CPU时间、实际使用时间、时间限制、记账数据、作业或进程数量等

  • I/O状态信息:包括显式的I/O请求、分配给进程的I/O设备(例如磁盘驱动器)和被进程使用的文件列表等

  • 内存指针:包括程序代码和进程相关数据的指针

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

  • ……

3.2 进程调度

1. 调度队列

  • 作业队列:进程进入系统时被加入到作业队列中,该队列包含系统中所有进程。

  • 就绪队列:驻留在内存中等待运行的程序保存在就绪队列中,该队列常用链表来实现,其头节点指向链表的第一个和最后一个PCB块的指针。每个PCB包括一个指向就绪队列的下一个PCB的指针域。

  • 设备队列: 操作系统也有其他队列。等待特定IO设备的进程列表称为设备队列。每个设备都有自己的设备队列。

在Linux 中每一个进程都由task_struct 数据结构来定义. task_struct就是我们通常所说的PCB,还包含有指向父进程和子进程的指针。

新进程开始处于就绪队列,它就在就绪队列中等待直到被选中执行或被派遣。当进程分配到cpu执行时,可能发生:

a.进程发出一个IO请求,并放到IO队列中。

b.进程创建新的子进程,并等待其结束

c.进程由于中断而强制释放cpu,并被放回到就绪队列中

对于前两种情况,进程最终从等待状态切换到就绪状态,并放回到就绪队列中。进程继续这一循环直到终止,到那时它将从所有队列中删除,其PCB和资源将得以释放。

进程队列

2. 调度程序

进程会在各种调度队列之间迁移,为了调度,操作系统必须按某种方式从这些队列中选择进程。进程的选择是由相应的调度程序(scheduler)来执行的。

通常批处理系统中,进程更多的是被提交,而不是马上执行。这些进程通常放到磁盘的缓冲池里,以便以后执行。长期调度程序或作业调度程序从该池中(工作队列?)选择进程,并装入内存以准备执行。短期调度程序或cpu调度程序从准备执行的进程中(就绪队列?)选择进程,并为之分配cpu。

这两个调度程序的主要差别是调度的频率。

短期调度程序通常100ms至少执行一次,由于每次执行之间的时间较短,短期调度程序必须要快。

长期调度程序执行的并不频繁,所以长期调度程序能使用更多的时间来选择执行进程。长期调度程序控制多道程序设计的程度(内存中进程的数量)。长期调度程序必须仔细选择执行进程。通常,绝大多数进程可分为:I/O为主或CPU为主

对于Linux和Windows系统通常没有长期调度程序,这些系统的稳定性依赖于物理限制,如果系统性能下降很多,会有用户的退出。

3. 上下文切换

中断使CPU从当前任务改变为运行内核子程序。当发生一个中断时,系统需要保存当前运行在CPU中进程的上下文,从而能在其处理完后恢复上下文。进程的上下文用PCB来表示。通常通过执行一个状态保存(state save)来保存cpu当前状态,之后执行一个状态恢复(state restore)重新开始运行。

将CPU切换到另一进程需要保存当前状态并恢复另一进程状态,这叫做上下文切换(context switch)。当发生上下文切换时,内核会将旧进程的状态保存在PCB中,然后装入经调度要执行的并已保存的新进程的上下文。

上下文切换时间是额外开销,因为切换时系统并不能做什么有用的工作。其切换时间与硬件支持密切相关。

4. 进程创建

进程在执行时,能通过创建进程系统调用创建多个新进程。创建进程为父进程,而新进程叫做子进程。新进程都可再创建其他进程,从而形成了进程树

大多数操作系统根据一个唯一的进程标识符(process indentifier,pid)来识别进程,pid通常是一个整数值。

在UNIX中,使用ps命令可以得到一个进程列表。

通常进程需要一定的资源(如CPU时间,内存,文件,I/O设备)来完成其任务。子进程被创建时,子进程可能直接从操作系统,也可能只从父进程那里获取资源。父进程可能必须在其子进程之间分配资源或共享资源(如内存或文件),限制子进程只能使用父进程的资源能防止创建过多的进程带来的系统超载。

在进程创建时,除了得到各种物理和逻辑资源外,初始化数据(或输入)由父进程传给子进程

当进程创建新进程被创建时,有两种执行可能:

a.父进程与子进程并发执行

b.父进程等待,直到某个或全部子进程执行完

新进程的地址空间也有两种可能:

a.子进程是父进程的复制品(具有与父进程相同的程序和数据)。

b.子进程装入另外一个新程序

UNIX操作系统中,每个进程用唯一整数标识符来标识,通过fork()系统调用可创建新进程,新进程通过复制原来进程的地址空间而成。这种机制允许父子进程之间方便的通信。

两个进程都继续执行位于系统调用fork()之后的指令,但是对于子进程,系统调用fork的返回值为0;而对于父进程,返回值为子进程的进程标识符(非零)。

通常系统调用fork后,一个进程会使用系统调用exec(),以用新程序来取代进程的内存空间。系统调用exec()将二进制文件装入内存(消除了原来包含系统调用exec()的程序内存映射),并开始执行。采用这种方式,两个进程都能相互通信,并按各自的方式执行。

父进程能创建更多的子进程,或者如果在子进程运行时没有什么可做,那么它采用系统调用wait()把自己移出就绪队列来等待子进程的终止。

/*
* Filename
: pctl.c
* 
: (C) 2015 by 孙铭超
* Function
: 编写一个多进程并发执行程序。父进
 程首先创建一个执行 ls 命令的子进程然后再创建一个执行 ps 命令的子进程,
 并控制ps 命令总在 ls 命令之前执行。
*/
#include < sys/types.h >
#include < wait.h >
#include < unistd.h >
#include < signal.h >
#include < stdio.h >
#include < stdlib.h >

int main(int argc)
{

    int i;
    
    int pid_ls;//存放执行ls命令子进程号    
    int pid_ps;//存放执行PS命令子进程号    
    int status_ls; //存放ls子进程返回状态    
    int status_ps; //存放ps子进程返回状态    
    char *args_ls[] = {"/bin/ls","-a",NULL}; //子进程要缺省执行的命令ls    
    char *args_ps[] = {"/bin/ps","-l",NULL}; //子进程要缺省执行的命令ps    
    pid_ls=fork() ; //建立ls子进程
    
    if(pid_ls<0){ // 建立ls子进程失败    
        printf("Create Process(ls) fail!\n");
        exit(EXIT_FAILURE);   
    }else if(pid_ls == 0){ // ls子进程执行代码段        
        printf("I am Child process %d\nMy father is %d\n",getpid(),getppid());    //报告父子进程进程号            
        pid_ps=fork() ; //创建ps子进程    
        if(pid_ps<0){ // 建立ps子进程失败            
            printf("Create Process(s) fail!\n");
            exit(EXIT_FAILURE);
        }else if(pid_ps == 0){ // ps子进程执行代码段   
            printf("I am Child process %d\nMy father is %d\n",getpid(),getppid());     //报告父子进程进程号
            printf("%d child_ps will Running: \n",getpid());    //子进程执行ps                       
            for(i=0; args_ps[i] != NULL; i++) //则执行缺省的命令ps
                printf("%s ",args_ps[i]);
            printf("\n");                       
            status_ps = execve(args_ps[0],args_ps,NULL); //装入并执行新的程序           
        }else{                        
            printf("%d Waiting for child %d to be done.\n\n",getpid(), pid_ps); //父进程等待子进程执行结束
            waitpid(pid_ps,&status_ps,0); //等待子进程结束
            printf("\nMy child exit! status = %d\n\n",status_ps);            
        }
        //子进程执行ls
        printf("%d child_ls will Running: \n",getpid()); //
        
        //则执行缺省的命令ls
        for(i=0; args_ls[i] != NULL; i++) printf("%s ",args_ls[i]);
        printf("\n");                
        status_ls = execve(args_ls[0],args_ls,NULL); //装入并执行新的程序
        }
    
    else //父进程执行代码段
    {
        printf("\nI am Parent process %d\n",getpid()); //报告父进程进程号        
        sleep(1) ; //等待子进程建立        
        printf("%d don't Wait for child(ls) done.\n\n",getpid()); //与子进程并发执行不等待子进程执行结束
    }
    
    return EXIT_SUCCESS;
}

执行结构

I am Parent process 29814
I am Child process 29815
My father is 29814
29815 Waiting for child 29816 to be done.

I am Child process 29816
My father is 29815
29816 child_ps will Running: 
/bin/ps -l 
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000 25134 25128  0  80   0 -  6968 wait   pts/1    00:00:00 bash
0 S  1000 29814 25134  0  80   0 -  1048 hrtime pts/1    00:00:00 deom1
1 S  1000 29815 29814  0  80   0 -  1048 wait   pts/1    00:00:00 deom1
0 R  1000 29816 29815  0  80   0 -  1783 -      pts/1    00:00:00 ps

My child exit! status = 0

29815 child_ls will Running: 
/bin/ls -a 
.  ..  demo1.c  demo1.c~  deom1
29814 don't Wait for child(ls) done.

5. 共享内存系统

共享内存系统需要建立共享内存区域。通常一块共享内存区域驻留在生成共享内存段进程的地址空间。其他希望使用这个共享内存段进行通信的进程必须将此放到它们自己的地址空间上。数据的形式或位置取决于这些进程而不是操作系统,进程还负责保证他们不向同一区域同时写数据。

生产者—消费者问题是协作进程的通用范例。生产者进程产生信息以供消费者进程消费。例如,编译器产生的汇编代码供汇编程序使用,而汇编程序反过来产生目标代码供连接和装入程序使用。

采用共享内存是解决生产值——消费者问题方法之一。为允许生产者进程和消费者进程能并发执行,必须有一个缓冲来被生产者填充并被消费者所使用。此缓冲驻留在共享内存区域,消费者使用一项时,生产者能产生另一项。生产者和消费者必须同步,以免消费者消费一个没有生产出的项。

可以使用两种缓冲:

无限缓冲对缓冲大小没有限制。消费者可能不得不等待新的项,但生产者总可以产生新的项。

有限缓冲假设缓存大小固定。对于这种情况,如果缓冲为空,那么消费者必须等待,如果缓冲为满,那么生产者必须等待。

# define BUFFER-SIZE 10

typedef struct     {    
...    
}  item;   
  
item buffer[BUFFER-SIZE];    
int  in=0;
int out=0;

共享缓存通过循环数组和两个逻辑指针in和out来实现,in指向缓冲中下一个空位;out指向缓冲中第一个满位。当in = = out时,缓冲为空;当(in+1)%BUFFER-SIZE = = out时,缓冲为满。

生产者和消费者代码如下:生产者进程有一个局部变量nextProduced以储存所产生的新项。消费者有一个局部变量nextConsumed以存储要使用的新项。

生产者进程:

while (true) {   /* Produce an item */
    while (((in = (in + 1) % BUFFER SIZE count)  = = out); /*do nothing */
    buffer[in] = item;
    in = (in + 1) % BUFFER SIZE;
}

消费者进程:

while (true) {
    while (in == out); // do nothing -- nothing to consume
    // remove an item from the buffer
    item = buffer[out];
    out = (out + 1) % BUFFER SIZE;
    return item;
}

这种方法允许缓存的最大项数是BUFFER-SIZE-1


lindsay_bubble
26 声望11 粉丝