操作系统抽象之进程

一、进程的概念

首先思考一个问题:CPU的核心数是有限的,那么在运行远超过CPU核心数数量的程序时,操作系统是如何实现CPU核心数仿佛无限的假象的?

当然是通过虚拟化CPU来实现,也就是让一个程序只运行一个时间片,然后切换到其他程序,通过高速的上下文切换来伪造一种多个CPU的假象,这也就是时分共享(time sharing)的CPU技术。这种行为潜在会造成一些性能的损失,也就是不停切换时耗费的COU性能。

CPU的虚拟化想要实现,操作系统需要一些低级机制与高级技能。机制是一些低级方法或协议,用于实现所需的功能,例如上下文切换(context switch),它让操作系统可以停止运行一个程序而切换到另一个程序继续运行;目前所有的现代操作系统都有这种分时机制。时分共享相对应的还有空分共享,资源在空间上被划分给希望使用它的人,例如磁盘空间就是一个空分共享资源,将一块磁盘分配给文件后,在用户删除文件之前不会再将其分配给其他文件。高级智能一般以策略的形式存在,策略就是操作系统做出某种决定时所使用的算法。例如,当前有一组程序在CPU上运行,操作系统该如何决定程序运行的顺序?这时就会用到调度策略(scheduling policy),调度策略有多种,可能会根据历史信息(最近一次被调用的程序是哪个)、工作负载(运行什么类型的程序)、性能(程序所占用的系统资源大小)等等来决定最终的结果。在大多数操作系统中,策略与机制是要分开的,这种模块化的形式也是当今软件的一种通用原则。例如开篇的问题答案,操作系统的机制就是上下文切换,而对应的策略是切换时选择哪一个进程被选中;机制与策略共同实现了操作系统想要的结果,然而进程与策略彼此的操作又互不影响。

操作系统对正在运行的程序的抽象,就是进程,也就是说进程就是一个正在运行的程序。要理解进程的构成,就要知道进程的机器状态(machine state):程序在运行时可以读取或更新的内容。机器状态有一个明显的组成部分,就是内存。指令需要存在内存中,程序读取和写入的数据也在内存中,所以进程可以访问的内存自然就是进程的一部分。机器状态的另一部分是寄存器,计算机很多指令明确的读取或更新寄存器,所以寄存器对进程也极为重要。

 

二、进程的创建

进程是一个运行的程序,那么想要得到一个进程就只要运行一个程序就可以了。操作系统要运行一个程序做的第一件事就是将代码和所有的静态数据加载到内存中,内存也就成为了进程的地址空间。当然这个加载过程也是极为复杂的,但是目前不需要了解,只需要知道在运行程序之前,操作系统会将代码和数据通过某种方式加载到了内存中。然后就需要为程序分配运行时栈(run-time stack),在C++中,栈用来存放局部变量、函数参数和返回地址,操作系统将这些内存分配给进程,当然在某些情况下栈是可以由用户制定分配的,例如VS设置的堆栈空间大小,通过_beginthread创建线程时第二个参数所指定的线程栈空间等等。之后操作系统还会为程序分配一些堆内存,在C++中,堆内存用于动态申请分配的数据,也就是new所得到的的内存空间。再然后操作系统还会执行一些其他的初始化任务,特别是输入/输出(I/O)相关的。例如:UNIX系统中,默认情况下会为每个进程分配3个打开的文件描述符(file descriptor),用于标准输入、输出和错误,这些描述符让程序轻松读取来自中断的输入以及打印输出到屏幕。在这些都结束之后,那么运行一个程序的准备工作就结束了,这是就可以启动程序,例如C++/C系列代码会通过Main()函数进入程序,进入Main()之后,操作系统就将CPU的使用权转交给了运行起来的程序,程序就可以执行下去了。

 

三、进程状态

进程会处于以下三种状态之一:

●  运行(running) :在运行状态下,进程正在处理器上运行,也就是正在执行指令。

●  就绪(ready):就绪状态下,进程已准备好运行,但由于某些原因此时处理器的时间片并没有分给进程。

●  阻塞(blocked): 阻塞状态下,一个进程执行了某种操作,直到发生了其他事件才会准备运行。

状态映射图如下:

进程会在如图的状态下进行转换。从就绪到运行意味着该进程已经被调度,从运行转移到就绪意味着该进程已经取消调度(一般是由于处理器时间片耗光)。一旦进程发起某些操作,例如IO请求,进程就会处于阻塞状态,知道IO完成,就会才会结束阻塞。在这些过程中,操作系统起到的是做决策作用。例如当在一个内核中同时存在两个进程时,假设两个进程都处于执行状态,那么何时由第一个进程切换到下一个进程,就是由操作系统决定,一般是第一个进程将操作系统分配给它的时间片耗光就会切换,这也是很简单的时间片轮转策略;如果第一个进程开始进行IO处理,那么此时即使进程一拥有未用完的时间片,也是无效的,因为此时接下来的时间片内进程一只会空占着处理器,而需要等待IO完成才能继续执行程序,那么此时操作系统就可以决定是让进程一等到时间片耗光还是立刻切换到进程二;在之后,当进程一的IO完成,而此时是由进程二占用着时间片,那么是否立即将处理器的控制权交给进程一,这也是由操作系统决定。这些都是操作系统的调度策略。

其实进程还有一些其他的状态,只是这些状态在进程的生命周期中占用的份额极少,所以几乎可以忽略。例如进程的初始状态(initial),表示进程在创建过程中处于的状态。还有一个进程可以处于已退出但是尚未清理的最终状态(final,在UNIX中这种状态成为僵尸状态),这个最终状态也是很有用的,可以给其他进程检查他的返回码的时间,从而让其他进程知道其运行结果。

 

四、进程API

1.windows

windows下可以通过CreateProcess 来创建进程,

通过 WaitForSingleObject 等待进程结束;


#include <iostream>
#include <time.h>
#include <windows.h>
#include <process.h>
#include "assert.h"
using namespace std;

int main()
{
    cout << "this is a process ID : %d" << _getpid() << endl;
	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));
	LPTSTR szCmdline = _T("D:\\Setup.exe");
	if (!CreateProcess(NULL,  
		szCmdline,       
		NULL,          
		NULL,           
		FALSE,          
		0,              
		NULL,         
		NULL,          
		&si,          
		&pi)          
		)
	{
		printf("CreateProcess failed (%d).\n", GetLastError());
		assert(0);
	}

	WaitForSingleObject(pi.hProcess, INFINITE);
 
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);

    return 0;
}

2.linux

linux 下可以通过 fork(), vfork() 来创建进程,fork()的返回值 : pid_t fork(void)   创建成功返回0, 失败返回-1, 父进程返回子进程的pid;vfork()创建成功会返回0。 fork从已经存在的进程中创建一个新的进程;vfork()则创建子进程,子进程和父进程公用同一块虚拟地址空间,为了防止调用栈混乱,因此阻塞父进程直到子进程退出调用exit() 退出 或者 进行程序替换;

fork 与 vfork的区别:

1. fork()创建出来的子进程和父进程谁先运行不一定

2. fork() 创建进程是将父进程的所有数据拷贝一份, 包括虚拟地址空间和页表, 这个时候他们两个里面所有的数据的虚拟地址都是一样的,但是当子进程对一个变量进行修改的时候, 这个时候系统会为这个 变量重新开辟空间, 也就是我们上篇博客中所说的 写时拷贝技术, 子进程与父进程代码共享, 数据独有

3. vfork()创建出来的子进程与父进程公用同一块虚拟地址空间, 这个时候我们在子进程中对数据进行拷贝的时候,父进程中会随着一起改变,有可能会造成函数调用栈混乱, 所以当fork实现了写时拷贝技术之后vfork基本就被淘汰了。

4. vfork存在的意义是快速创建子进程, 因为公用一块虚拟地址空间, 减少了子进程拷贝父进程的消耗, 所以速度快

5. vfork创建出子进程后一定是子进程先运行, 等到子进程exit退出或者exec函数族程序替换之后父进程才会开始运行。
 

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
 
int main()
{
    pid_t pid = fork();
    if(pid< 0)
    {
        perror("fork error");
        return -1;
    }
    else if(pid == 0)
    {
        printf("i am child\n");
    }
    else
    {
        printf("i am parent\n");
    }
    return 0;
}

 可以通过

exit()        exit是库函数接口, 底层也是调用_exit,但是调用前会刷新缓冲区,做退出前的收尾工作

_exit()      _exit是系统调用接口, 直接退出, 释放资源

来终止进程。

通过  pid_t wait(int *status); 来等待进程

    返回值 : 成功返回子进程pid,  失败返回-1

    status : 子进程退出码, 输出型参数, 如果不关心子进程返回值可以置为NULL

注意  wait等待子进程是一个阻塞等待, 死等, 如果子进程没有退出父进程不会运行.

也可以通过  pid_ t waitpid(pid_t pid, int *status, int options);等待

waitpid返回值 : 正常返回的时候返回子进程的进程ID,如果设置了选项 :  WNOHANG, 如果没有发现已经退出的子进程则返回0,如果调用出错 : 返回-1, 这时error会被置成异常退出信号值;

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <wait.h>
 
int main()
{
  int pid = fork();
  if(pid < 0)
  {
    perror("fork error");
    exit(-1);
  }
  else if(pid == 0)
  {
    sleep(50);
    exit(0);
  }
 
  int statu;
  int ret; 
  while((ret = waitpid(pid,&statu,WNOHANG)) == 0)
  {
    printf("等一下\n");
    sleep(1);
  }
  printf("%d--%d\n",ret,pid);
  return 0;
}

 

 

 

©️2020 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值