操作系统入门(一)

操作系统介绍

程序运行时会发生什么?

Von Neumann计算模型基本理念:

graph LR;
	取指令 --> 解码 --> 执行

完成一条指令后,处理器将继续执行下一条指令,直到程序完成运行。

一种允许程序与设备交互,让程序共享内存,使程序运行变得容易的软件,这种软件被称为操作系统(Operating System, OS ),操作系统负责确保系统既易于使用又高效的运行。

虚拟化

关键问题:如何实现资源的虚拟化?

  • 通过虚拟化技术(virtualization)将物理资源(cpu、内存、磁盘等)转换为虚拟形式,因此有时也将操作系统称为虚拟机。

  • 为了方便用户使用,操作系统提供了一个标准库(standard library),其中包含几百个系统调用(也称之为接口(API)),以供程序调用来实现程序的运行、物理资源的访问等。

  • 虚拟化让多个程序运行时共享物理资源,操作系统也被称为资源管理器,cpu,磁盘等都是系统的资源,操作系统主要扮演资源的管理者,做到高效或者公平。
实例1:CPU的虚拟化

查看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: cpu <string>\n");
exit(1);
}
char *str = argv[1];

while (1) {
printf("%s\n", str);
Spin(1);
}
return 0;
}

该程序简单调用Spin()函数,反复检查时间并在运行一秒后返回,接着在屏幕上打印。

  • 查看运行结果(之后所有代码运行环境皆为Ubuntu20.04)
1
2
3
4
5
6
$ ./cpu "a"
a
a
a
a
^C

运行时该程序会重复检查时间,直到一秒后,该程序会打印用户输入的参数,并且继续运行。

实例2:内存的虚拟化

现代及其提供的物理内存模型非常简单,内存就是一个字节数组,通过地址访问相应的数据,进行更新或者写入操作时还需要给定相应地址的数据。程序运行时所有结构都保存在内存中,并且通过各种指令来访问,每次读取指令都会访问内存。

查看如下代码:

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

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: mem <value>\n");
exit(1);
}
int *p;
p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) addr pointed to by p: %p\n", (int) getpid(), p);
*p = atoi(argv[1]); // assign value to addr stored in p
while (1) {
Spin(1);
*p = *p + 1;
printf("(%d) value of p: %d\n", getpid(), *p);
}
return 0;
}

该程序使用malloc()分配一些内存,运行结果如下:

1
2
3
4
5
6
7
8
9
$ ./mem 1
(427) addr pointed to by p: 0x55f4929532a0
(427) value of p: 2
(427) value of p: 3
(427) value of p: 4
(427) value of p: 5
(427) value of p: 6
(427) value of p: 7
^C

该程序首先分配一些内存,之后打印出内存对应的地址,将数字0放入分配的内存中第一个空位,最后延迟一秒并递增p中保存的地址值,每个语句还会打印正在运行程序的标识符(PID),对于每个进程PID为唯一

并发

关键问题:如何构建正确的并发程序

如何构建正确的程序?操作系统需要提供什么?硬件需要有什么机制?如何利用他们解决并发问题?

  • 一个多线程程序
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
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include "common_threads.h"

volatile int counter = 0;
int loops;

void *worker(void *arg) {
int i;
for (i = 0; i < loops; i++) {
counter++;
}
return NULL;
}

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: threads <loops>\n");
exit(1);
}
loops = atoi(argv[1]);
pthread_t p1, p2;
printf("Initial value : %d\n", counter);
Pthread_create(&p1, NULL, worker, NULL);
Pthread_create(&p2, NULL, worker, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("Final value : %d\n", counter);
return 0;
}

主程序利用Pthread_create()创建两个线程,每个线程都在work()函数中运行,由用户的输入决定work()函数中循环的运行次数,最后输出玄幻次数counter,查看不同参属下运行的次数:

  • loop = 1000
1
2
3
$ ./threads 1000
Initial value : 0
Final value : 2000
  • loop = 5000
1
2
3
$ ./threads 5000
Initial value : 0
Final value : 10000
  • loop = 10000
1
2
3
$ ./threads 10000
Initial value : 0
Final value : 20000
  • loop = 100000
1
2
3
4
5
6
$ ./threads 100000
Initial value : 0
Final value : 106116
$ ./threads 100000
Initial value : 0
Final value : 127115

可以看到当循环次数较小时候,当loop = n时,输出为2n,这非常好理解,因为共有两个线程,当loop值更大时,我们使输入为100000,最终结果不为200000,并且再次运行,结果又产生了不同。

1
for (i = 0; i < loops; i++)

造成这种现象的原因是该语句的执行过程需要三条指令集控制:

graph LR;
将数值加载到寄存器 --> 将数值递增 --> 将数值放回内存

这一过程不具有原子性(automically)

持久性

关键问题:如何持久的存储数据

持久性的实现需要哪些技术?那些策略和机制能高性能的实现持久性?面对软硬件故障如何实现可靠性?

操作系统中的磁盘管理软件称为文件系统,负责将用户创建的任何文件存储在磁盘上。

操作系统不会为磁盘创建虚拟资源,用户之间需要共享文件系统中的信息,查看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>

int main(int argc, char *argv[])
{
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
assert(fd >= 0);
char buffer[20];
sprintf(buffer, "hello world\n");
int rc = write(fd, buffer, strlen(buffer));
assert(rc == (strlen(buffer)));
fsync(fd);
close(fd);
return 0;
}

该程序发出三个调用来实现了一个文件的写入,文件系统的实际写入过程如下:

graph LR;
确定数据的驻留位置  --> 发出IO请求 --> 读取现有结构 --> 在文件系统中对位置进行记录

出于性能原因,IO操作会延迟进行。为了预防IO操作中发生系统异常,多数文件系统拥有日志(journaling)或写时复制(copy-on-write)这些复杂的写入协议。为了使不同的同游操作更有效,文件系统采用了不同的数据结构和访问方法,从简单的列表到B-TREE。

操作系统设计目标

操作系统主要功能

  • 取得物理资源并进行虚拟化
  • 处理并发
  • 持久化存储文件

最基本目标:建立抽象

抽象使编写大型程序称为可能,下面是一个简单的抽象过程:

graph LR;
晶体管 --> 逻辑门 --> 汇编语言 --> 高级程序语言
  • 提供高性能
  • 在程序与OS之间提供保护
  • 可以不间断运行
  • 安全性
  • 移动性
  • …………

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!