进程管理

1. 进程概念

PCB,程序段,数据段三部分构成了进程实体

进程状态:

  • 运行态:占有cpu,并且就在cpu上执行。

  • 就绪态:进程等待分配cpu(也就是cpu没有调度到它)

  • 等待态:等待某个事件发生(如io完成或收到信号)

还有两个状态:

  • 创建态
  • 终止态

PCB:系统系统为每个运行的程序排至一个数据结构,称为进程控制块

PCB包含进程相关信息:

  • 进程状态
  • 进程计数器:要执行的下个指令的地址
  • CPU寄存器:状态信息在中断时要保存下来,方便进程以后能继续执行
  • CPU调度信息:包含进程优先级,调度队列指针等
  • 内存管理信息

2. 进程调度

调度队列

  • 作业队列:包含所有进程

  • 就绪队列:驻留在内存中就绪、等待运行的进程保存在这个队列。用循环链表表示,头节点指向链表第一个和最后一个PCB块的指针。

  • 设备队列:进程向磁盘发送IO请求,由于系统有很多进程,磁盘可能忙于其他IO请求,该进程就需要等待磁盘。等待IO设备的队列叫做设备队列。

调度程序

长期调度程序:频繁执行

短期调度程序:不频繁

什么是中断?

说到中断还不得不从现代操作系统的特性说起,无论是桌面PC操作系统还是嵌入式都是多任务的操作系统,而很遗憾,处理器往往是单个的,即使在硬件成本逐渐下降,从而硬件配置直线上升的今天,PC机的核心可能已经达到4核心,8核心,而手机移动设备更不可思议的达到16核心,32核心,处理器的数量依然不可能做到每个任务一个CPU,所以CPU必须作为一种全局的资源让所有任务共享。说到共享,如何共享呢?什么时候给任务A用,什么时候给任务B用……这就是进程调度,具体的安排就由调度算法决定了。进程如何去调度?现代操作系统一般都是采用基于时间片的优先级调度算法,把CPU的时间划分为很细粒度的时间片,一个任务每次只能时间这么多的时间,时间到了就必须交出使用权,即换其他的任务使用。这种要看操作系统的定时器机制了。那么时间片到之后,系统做了什么呢?这就要用到我们的中断了,时间片到了由定时器触发一个软中断,然后进入相应的处理历程。当然这一点不足以表明中断的重要,计算机操作系统自然离不开外部设备:鼠标、键盘、网卡、磁盘等等。就拿网卡来讲,我计算机并不知道时候数据包会来到,我能保证的就是数据来了我能正常接收就行了。但是我又不可能一直等着接收数据包,要是这样其他任务就死完了。所以合理的办法是,你数据包来到之后,通知我,然后我再对你处理,怎么通知呢??答:中断!键盘、鼠标亦是如此!

当发生一个中断时,系统需要保存当前运行在CPU中进程的上下文,从而在其处理完后能恢复上下文,即先中断进程,之后再继续。进程上下文用PCB表示,它保存CPU的寄存器,进程状态等信息。

上下文切换:将CPU切换到另一个进程需要保存当前进程的状态,并恢复另一个进程状态。这一任务切换成为上下文切换。发生上下文切换时,内核会将旧进程的装填保存在其PCB中,然后装入经调度要执行的新进程的PCB。上下文切换的时间是额外的开销,切换时系统不能做什么有用的工作。

3. 进程操作

3.1进程创建

是什么:进程在其执行过程中,可以创建多个新进程。创建进程称为父进程,新进程称为子进程。

操作系统根据唯一**进程标识符(PID)**来识别进程。

使用 fork 系统调用创建进程

  • fork 系统调用是用于创建进程
  • fork 创建的进程初始化状态是和父进程一样的(进程有进程空间、内存、内存态等)
  • 系统会为 fork 的进程分配新的资源(包括内存资源、CPU 资源等)
  • fork 系统调用无参数
  • fork 会返回两次,分别返回子进程 id 和 0(第一次是由父进程返回的,第二次由子进程所返回的,因此返回了两次)
  • 返回子进程 id 的是父进程,返回 0 的是子进程

调用 fork 之后,我们就可以根据返回值是否为 0 来判断是父进程还是子进程返回的

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<cstring>
#include<stdio.h>
#include<unistd.h>

using namespace std;

int main()

    pid_t pid;
    pid = fork();
    if(pid == 0) {
        cout << "这是一个子进程" << endl;
    }
    else if(pid > 0) {
        cout << "这是一个父进程" << endl;
        cout << "子进程id:" << pid << endl;
    }
    else if(pid < 0 ){
        cout << "创建进程失败" << endl;
    }
    return 0;
}

运行结果:

从运行结果可以看到,fork 确实是返回了两次,两个 if 里边都走到了

在前边也说到,当 fork 创建一个子进程的时候,这个子进程的初始化内存状态是和父进程一样的,下边也用代码验证一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
#include<cstring>
#include<stdio.h>
#include<unistd.h>

using namespace std;

int main()
{
    pid_t pid;
    int num = 888;
    pid = fork();
    if(pid == 0) {
        cout << "这是一个子进程" << endl;
        cout << "num in son process:"<< num << endl;
        while(true) {
            num+=1;
            cout << "num in son process:"<< num << endl;
            sleep(1);
        }
    }
    else if(pid > 0) {
        cout << "这是一个父进程" << endl;
        cout << "子进程id:" << pid << endl;
        cout << "num in father process:"<< num << endl;
        while(true) {
            num-=1;
            cout << "num in father process:"<< num << endl;
            sleep(1);
        }
    }
    else if(pid < 0 ){
        cout << "创建进程失败" << endl;
    }
    return 0;
}

运行结果:

从图中可以看到子进程的初始值是和父进程是一样的,然后随着父进程和子进程执行的逻辑不一样,他们的 num 值就分道扬镳了。也就是说初始化的状态,子进程的内存空间和父进程的内存空间是一样的,但是随着他们的逻辑走向不一样,他们的内存空间将走向不一样

在快速变化的技术中寻找不变,才是一个技术人的核心竞争力。知行合一,理论结合实践

参考: https://xie.infoq.cn/article/c0d9a809d7cb57f29432e22f3

3.2 进程终止

当进程完成执行最后语句并使用exit请求操作系统删除自身时,进程终止。此时,进程可以返回状态值到父进程(通过系统调用wait())。

其他情况也会出现终止。例如父进程可以通过系统调用,终止子进程。父进程需要知道子进程pid,所以当进程创建新进程时,新进程的pid要传递给父进程。

为什么要终止子进程?

  • 子进程使用超过它所分配的资源
  • 分配给子进程的任务不再需要
  • 父进程退出。父进程退出后,操作系统不允许子进程继续执行。

3.3 进程间通信

如果一个进程能够影响其他进程或者受其他进程影响,那么该进程是协作的。

为什么需要协作:

  • 信息共享
  • 计算加速
  • 模块化
  • 方便

如何协作?进程间通信(IPC)

进程间通信有两种基本模型:

  • 共享内存:速度快于消息传递,这是因为消息传递的实现要经常采用系统调用,因此需要更多时间以便内核接入。与此相反,共享内存系统仅在创建共享内存区域时需要系统调用,一旦建立共享内存,所有访问都可以作为常规内存访问,无需借助内核。
  • 消息传递:对于交换数据量较少数量的数据很有用,因为无需避免冲突。对于分布式系统,消息传递也比共享内存更容易实现。

最新研究表明:在多核系统上,消息传递的性能是要优于共享内存。共享内存有高速缓存一致性的问题,这是由于共享数据在多个高速缓存之间迁移而引起的。

3.3.1 共享内存系统

采用共享内存的进程间通信,需要建立共享内存区域。多个进程通过在共享区域内读出或写入来交换信息。另外,进程负责确保,它们不向同一位置同时写入数据。

生产者-消费者模型:

writer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define BUFSZ 512

int main(int argc, char *argv[]) {
    int shmid;
    int ret;
    key_t key;
    char *shmadd;

    //创建key值
    key = ftok("../", 2015);
    if (key == -1) {
        perror("ftok");
    }

    //创建共享内存
    shmid = shmget(key, BUFSZ, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(-1);
    }

    //将共享内存端挂载到自己地址空间
    // 第一次创建共享内存段时,它不能被任何进程访问。要想启动对该内存的访问,必须将其连接到一个进程的地址空间
    shmadd = shmat(shmid, NULL, 0);
    if (shmadd < 0) {
        perror("shmat");
        _exit(-1);
    }

    //拷贝数据至共享内存区
    printf("copy data to shared-memory\n");
    bzero(shmadd, BUFSZ); // 共享内存清空
    strcpy(shmadd, "how are you, lh\n");

    return 0;
}

reader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define BUFSZ 512

int main(int argc, char *argv[]) {
    int shmid;
    int ret;
    key_t key;
    char *shmadd;

    //创建key值
    key = ftok("../", 2015);
    if (key == -1) {
        perror("ftok");
    }

    system("ipcs -m"); //查看共享内存

    //打开共享内存
    shmid = shmget(key, BUFSZ, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(-1);
    }

    //映射
    shmadd = shmat(shmid, NULL, 0);
    if (shmadd < 0) {
        perror("shmat");
        exit(-1);
    }

    //读共享内存区数据
    printf("data = [%s]\n", shmadd);

    //分离共享内存和当前进程
    ret = shmdt(shmadd);
    if (ret < 0) {
        perror("shmdt");
        exit(1);
    } else {
        printf("deleted shared-memory\n");
    }

    //删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    system("ipcs -m"); //查看共享内存

    return 0;
}

运行writer:

运行reader:

https://www.cnblogs.com/tp-16b/p/8987697.html

https://zhuanlan.zhihu.com/p/37808566

3.3.2 消息传递系统

消息传递提供一种机制,以便允许进程不必通过共享内存来实现通信。对分布式环境,这特别有用。

消息传递至少提供两种操作:

send(message) receive(message)

进程通过系统提供的发送消息和接收消息两个原语进行数据交换。

  1. 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。

  2. 间接通信方式:发送进程把消息发送到某个中间实体中,接收进程从中间实体中取得消息。这种中间实体一般称为信箱,这种通信方式又称为信箱通信方式。该通信方式广泛应用于计算机网络中,相应的通信系统称为电子邮件系统。

3.4 客户机/服务器通信

之前介绍了,进程可以通过共享内存和传递消息进行通信。

那么客户端/服务端如何通信呢?

3.4.1 套接字

通过网络通信的每对进程需要使用一对套接字。套接字由一个IP地址和一个端口号组成。通常套接字采用C/S架构。

特点:虽然常用和高效,但是属于分布式进程间的一种低级形式的通信。一个原因是,套接字只允许在通信线程之间交换无结构的字节流。

SocketClient.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.net.*;
import java.io.*;

public class SocketClient {
    public static void main(String[] args) {
        try {
            // this could be changed to an IP name or address other than the localhost
            Socket sock = new Socket("127.0.0.1", 6013);
            InputStream in = sock.getInputStream();
            BufferedReader bin = new BufferedReader(new InputStreamReader(in));

            String line;
            while ((line = bin.readLine()) != null)
                System.out.println(line);

            sock.close();
        } catch (IOException ioe) {
            System.err.println(ioe);
        }
    }
}

SocketServer.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import java.net.*;
import java.io.*;

public class SocketServer {
    public static void main(String[] args) {
        try {
            ServerSocket sock = new ServerSocket(6013);

            // now listen for connections
            while (true) {
                Socket client = sock.accept();
                // we have a connection

                PrintWriter pout = new PrintWriter(client.getOutputStream(), true);
                // write the Date to the socket
                pout.println(new java.util.Date().toString());

                // close the socket and resume listening for more connections
                client.close();
            }
        } catch (IOException ioe) {
            System.err.println(ioe);
        }
    }
}

3.4.2 远程过程调用

远程过程调用类似于IPC机制,而且通常建立在IPC之上。不过,因为现在的情况是进程处在不同的系统上,所以应该提供基于消息的同新方案,以提供远程服务。

与IPC消息不一样,RPC通信交换的信息有明确的结构,而不仅仅是数据包。

RPC允许客户调用位于远程主机的过程,就像调用本地过程一样。通过客户端提供的存根(stub),RPC系统隐藏通信细节。

操作系统保证,每个消息执行正好一次(exactly once),而不是最多一次(at most once)。

RPC调用过程:

https://zhuanlan.zhihu.com/p/107040148

3.4.3 管道

管道(pipe)允许两个进程进行通信。管道是早期UNIX系统最早使用的一种IPC机制。管道为进程的通信提供了一种简单的方法,但是也有一定的局限性。

参考链接:

本书的github地址:https://github.com/greggagne/osc10e