操作系统-进程


进程

  • 独立的应用程序在数据集合上的独立执行。进程是操作系统进行资源分配和调度的基本单位,是应用程序运行的实体。一个进程可以包含一个或多个线程(Thread),线程是进程中的实际执行单元。
进程内部保存:
  1. 段表
  2. 共享库
  3. 初始化数据
  4. 代码

进程隔离

一般来讲就是段表,映射时候进行一个内存隔离。

特点

动态性
并发性
独立性
制约性

进程控制块

进程控制块(Process Control Block,简称 PCB)是操作系统中用于描述进程状态和控制进程运行的数据结构。PCB 是进程存在的唯一标志,它记录了操作系统在管理进程时所需的所有信息。当进程被创建时,操作系统会为其创建一个 PCB,当进程结束时,操作系统会回收其 PCB。

  1. 进程标识符
    与进程相关的唯一标识符pid
  2. 进程的状态
    进程目前处于运行态、等待态、就绪态等。
  3. 尤先级
    相对于其他进程的优先顺序
  4. CPU现场保护区
    工作寄存器、指令计数器和程序状态字等
  5. 内存指针
    当前队列指针、总链指针等
  6. 占用资源

进程状态

image.png

多进程

多个进程,一般分为就绪队列和磁盘等待队列(等待某个事件进行触发)

GUI图形界面使用

操作系统提供的图形界面方式的使用,本质上是通过(消息传递)机制实现的。

线程

什么是线程?

在进程内部增加一类实体,满足以下特性

(1)实体之间可以并发执行
(2)实体之间共享相同的地址空间
image.png

线程=进程-共享资源
线程的优点:
一个进程中可以同时存在多个线程
各个线程之间可以并发地执行
各个线程之间可以共享地址空间和文件等资源
线程的缺点:
一个线程崩溃,会导致其所属进程的所有线程
崩溃

进程和线程的比较

image.png

线程实现
用户线程:在用户空间实现

POSIX Pthreads,Mach C-
threads,Solaris threads

内核线程:在内核中实现

Windows,
Solaris Linux

用户线程

由用户级线程库函数完成线程的管理(线程的创建、终止、同步和调度等)

优点
■不依赖于操作系统的内核
内核不了解用户线程的存在
可用于不支持线程的多进程操作系统
■在用户空间实现的线程机制
每个进程有私有的线程控制块(TCB)列表
TCB由线程库函数维护
■同一进程内的用户线程切换速度快
无需用户态/核心态切换
■允许每个进程拥有自已的线程调度算法

用户级线程的缺陷在于,当一个进程中的某个线程发起资源请求,则整个进程都将进入(阻塞)状态。内核级线程是由系统完成线程的管理,所以,当一个进程中的某个线程发起资源请求时,系统将调度该进程中的另一个线程进入运行状态,(不会)【填”会/不会”】让整个进程进入(阻塞)状态。

缺点

  • 线程发起系统调用而阻塞时,则整个进程进入等待
  • 不支持基于线程的处理机抢占
    除非当前运行线程主动放弃,它所在进程的其
    他线程无法抢占CPU
  • 只能按进程分配CPU时间
    多个线程进程中,每个线程的时间片较少
内核线程

由内核通过系统调用实现线程机制,由内核完成线程创建、终止和管理

  • 由内核维护PCB和TCB
  • 线程执行系统调用而被阻塞不影响其他线程
  • 线程的创建、终止和切换相对较大
    通过系统调用/内核函数,在内核实现
  • 以线程为单位进行CPU时间分配
    多线程的进程可获得更多CPU时间

进程通信

共享存储

image.png

管道通信

image.png

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFER_SIZE 100

int main() {
int pipefd[2];
char buffer[BUFFER_SIZE];

pipe(pipefd); // 创建管道

pid_t pid = fork(); // 创建子进程

if (pid == 0) {
// 子进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello from child", 16);
close(pipefd[1]); // 关闭写端
exit(0);
} else if (pid > 0) {
// 父进程
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
read(pipefd[0], buffer, sizeof(buffer)); // 读取数据
printf("Received: %s\n", buffer);
close(pipefd[0]); // 关闭读端
} else {
// fork 失败
perror("fork");
exit(1);
}

return 0;
}

进程通信,利用管道

  • 子进程执行完毕:子进程完成其任务并退出,通常会调用 exit() 函数。

  • 父进程等待:如果父进程在调用 wait(),它会暂停执行,直到子进程结束。这样可以确保父进程在子进程完成后再继续读取数据或进行其他操作。

  • 子进程的退出状态:一旦子进程结束,父进程可以通过 wait() 获得子进程的退出状态

通信

  • 创建管道pipe(pipefd); 创建一个管道,pipefd[0] 用于读取,pipefd[1] 用于写入。

  • 创建子进程fork() 创建一个子进程。此时,子进程和父进程都有对管道的引用。

  • 关闭不需要的端

    • 子进程:在子进程中,使用 close(pipefd[0]); 关闭读端,只保留写端。这样,子进程只用来写数据。
    • 父进程:在父进程中,使用 close(pipefd[1]); 关闭写端,只保留读端。这样,父进程只用来读数据。
  • 写入数据:子进程执行 write(pipefd[1], "Hello from child", 16);,由于子进程关闭了读端,所以可以成功写入数据。

  • 关闭写端:在写完数据后,子进程关闭写端 close(pipefd[1]);,这一步是为了表明数据写入完毕。

  • 父进程读取数据:父进程在等待子进程完成后,读取管道中的数据。

意思就是我们的进程执行if 条件判断只是一个判断子进程和父进程谁先执行的一个标准,但是我们的子进程执行完之后会回到父进程。但是父进程需要wait()让子进程先执行完毕。同时父子进程都对管道有自己的读写权限。自己可以关闭打开各个开关。

消息传递

image.png

不同进程的通信方式

管道和套接字(socket)都是进程间通信(IPC)的方法,但它们在使用和功能上有所不同。管道通常用于在相关的进程(如父子进程)之间传输数据,而套接字则可以用于网络中的不同主机之间,或者在同一台机器上的无关进程之间进行通信

互斥 同步问题

进程互斥

进程互斥当某一进程正在访问某一存储区域时,不允许其他进程来读出或者修改存储区的内容
进程间的这种互相制约关系称为互斥

互斥:  一般是资源使用,只能独自占用。
同步: 一般是 相互等待同步进行。

  1.  锁机制
  2.  信号量机制
前置知识

临界资源
   一种只能单独使用的资源
临界区
  访问临界资源的程序段

针对临界资源进行操作

忙则等待

空闲等待

有限等待

让权等待

信号量

根据信号决定信号量,很多互斥情况一般为0或者1  。

例子:  (信号为互斥 设置量为1)

司机(p1)
汽车启动
行驶
停止

售票员(p2)

关门

售票

开门

两个资源  1. 关门 s1             2. 停车  s2
p2V(s1  0+1)  ->  p1  P(s1   1-1)
p1(V(s2  0+1))  -> p2(P(s2 1-1))

其实根据个人的理解来看,我们的s2 和s1 是信号 ,也是资源。我们都是在争夺对应的资源。同时这种资源并非只有在1或者0,多个量。

 引入休眠和唤醒的概念,一般来讲休眠,就是p操作,  唤醒就是v操作。根据个人理解来看,p操作导致休眠是一个信号量小于0,才导致休眠。否则就是占领。一般就是自身休眠也就是相当于等待。

s2和s1的初始值

全为0
流程图:

  1. P(s1) 司机休眠

  2. 关门

  3. V(S1)

  4. 售票

  5. P(S2) 休眠

  6. 启动

  7. 运行

  8. 到站

  9. V(S2)

  10.  司机end

  11. 售票员  开门

  12.  售票员 end

其实信号量是一个全局变量,可以变化相当于进程通信。 一般来讲,一个进程中想要占用资源,一般就是cpu或者是一个打印机。

互斥和同步的信号量

  1. 同步一般是并非是具体的资源而是一种信号,一般初始值设置为0就可以

  2. 互斥并非还抽象的资源而是具体的资源,一般来讲是需要根据资源数量来作为初值。

在互斥中,信号量的值负数可以代表有多少进程在等待。

生产者和消费者

同步问题:

信号量为s1 缓冲区为0 空和1 非空

生产者(P(s1))   1–  

V(s1)                 0++

信号量为s2  缓冲区 0空  1非空

消费者(P(s2))    

互斥问题:

我们把缓冲区个数作为信号量

进程同步

信号量
  • 定义:信号量是一个整数值,用于表示可用资源的数量。
  • 类型
    • 计数信号量:可以取任何非负整数值,表示多个相同资源的数量。
    • 二进制信号量(或互斥锁):只能取0和1两个值,常用于实现互斥访问。
P操作和V操作

P和V操作是信号量的基本操作,它们分别用于请求和释放资源。

  • P操作(也称为“等待”操作):

    • 作用:当一个进程希望访问某个资源时,它会执行P操作。如果信号量的值大于0,则将其减1并继续执行;如果信号量的值为0,则该进程将被阻塞,直到信号量的值大于0。
    • 表现P(S),表示对信号量S执行P操作。
  • V操作(也称为“信号”操作):

    • 作用:当一个进程完成对资源的使用后,它会执行V操作,将信号量的值加1。如果有其他进程因执行P操作而被阻塞,则会唤醒其中一个进程。
    • 表现V(S),表示对信号量S执行V操作。
示例

假设有一个简单的资源(比如打印机),只有一个进程可以同时使用:

  1. 初始化信号量:设定信号量S=1,表示打印机可用。
  2. 请求资源
    • 进程A调用P(S),信号量变为0,表示打印机正在被使用。
  3. 释放资源
    • 进程A使用完打印机后调用V(S),信号量变回1,表示打印机可用。
同步和互斥例子 生产者与消费者

生产者-消费者问题是计算机科学中的一个经典问题,它描述了两组进程(生产者和消费者)共享一个有限大小的缓冲区。生产者的任务是生成数据并将其放入缓冲区,而消费者的任务是从缓冲区取出数据并进行处理。这个问题涉及到两个关键的同步问题:互斥和同步。

同步(Synchronization)

同步是指协调多个线程的执行顺序,以确保它们能够按照正确的顺序执行。在生产者-消费者问题中,同步确保生产者在缓冲区满时等待,消费者在缓冲区空时等待。这需要一种机制来控制生产者和消费者之间的交互,确保它们不会在不适当的时间尝试访问缓冲区。

互斥(Mutex)

互斥是指在任何时候,只有单个线程可以访问共享资源。在生产者-消费者问题中,当生产者向缓冲区添加数据或消费者从缓冲区取出数据时,需要确保这些操作是互斥的,以防止多个生产者或消费者同时访问缓冲区,这可能导致数据不一致或缓冲区溢出。

问题描述

假设有一个固定大小的缓冲区,生产者可以生成数据并将其放入缓冲区,而消费者可以从缓冲区取出数据。如果没有适当的同步机制,可能会出现以下问题:

  1. 缓冲区溢出:如果生产者在缓冲区已满时继续添加数据,可能会导致缓冲区溢出。
  2. 数据丢失:如果消费者在缓冲区为空时尝试取出数据,可能会导致数据丢失。
  3. 竞争条件:如果没有互斥机制,多个生产者或消费者可能同时访问缓冲区,导致数据不一致。
解决方案

为了解决这些问题,可以使用信号量来实现互斥和同步:

  1. 互斥信号量:用于确保一次只有一个生产者或消费者访问缓冲区。这通常通过一个二进制信号量(初始值为1)来实现。
  2. 同步信号量:用于控制生产者和消费者之间的交互。通常需要两个信号量:一个用于表示缓冲区中可用的空位(初始值为缓冲区大小),另一个用于表示缓冲区中的数据项(初始值为0)。
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int in = 0; // 生产者放入数据的位置
int out = 0; // 消费者取出数据的位置

sem_t empty; // 空位信号量
sem_t full; // 满位信号量
pthread_mutex_t mutex; // 互斥锁

void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&empty); // 等待空位
pthread_mutex_lock(&mutex); // 进入临界区

// 生产数据
buffer[in] = i;
printf("Produced: %d\n", buffer[in]);
in = (in + 1) % BUFFER_SIZE;

pthread_mutex_unlock(&mutex); // 离开临界区
sem_post(&full); // 增加满位计数

sleep(1); // 模拟生产时间
}
return NULL;
}

void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&full); // 等待满位
pthread_mutex_lock(&mutex); // 进入临界区

// 消费数据
int data = buffer[out];
printf("Consumed: %d\n", data);
out = (out + 1) % BUFFER_SIZE;

pthread_mutex_unlock(&mutex); // 离开临界区
sem_post(&empty); // 增加空位计数

sleep(1); // 模拟消费时间
}
return NULL;
}

int main() {

pthread_t prod, cons;

// 初始化信号量和互斥锁
sem_init(&empty, 0, BUFFER_SIZE);
sem_init(&full, 0, 0);
pthread_mutex_init(&mutex, NULL);

// 创建生产者和消费者线程
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);

// 等待线程结束
pthread_join(prod, NULL);
pthread_join(cons, NULL);

// 清理资源
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);

return 0;
}

父子进程

什么是父进程和子进程?
  • 父进程:创建另一个进程的进程。
  • 子进程:由父进程创建的进程
fork 函数的作用

fork 是用于创建新进程的系统调用。它会复制当前进程,返回两次:

  • 在父进程中,返回子进程的 PID(进程标识符)。
  • 在子进程中,返回 0。

这意味着父子
进程几乎完全相同(包括执行的代码),但它们有不同的 PID 和某些独立的资源。

如何实现并发

在 C 语言中,通过 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
37
38

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define FILENAME "output.txt"

int main() {
// 打开文件以写入,使用 O_CREAT 创建文件,如果已存在则追加内容
int fd = open(FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

pid_t pid = fork(); // 创建新进程

if (pid < 0) { // fork失败
perror("fork");
close(fd);
exit(EXIT_FAILURE);
} else if (pid == 0) { // 子进程
const char *child_msg = "Hello from child!\n";
write(fd, child_msg, strlen(child_msg)); // 写入文件
printf("Child process wrote to file.\n");
} else { // 父进程
const char *parent_msg = "Hello from parent!\n";
write(fd, parent_msg, strlen(parent_msg)); // 写入文件
printf("Parent process wrote to file.\n");
}

close(fd); // 关闭文件描述符
return 0;
}

//fork 创建完毕进程之后,父子进程是一样的,此时我们有两个进程,需要操作系统进行并发调度。而刚好可以通过fork返回值判断父子进程。

文章作者: K1T0
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 K1T0 !
  目录