操作系统 | 概述

程序运行时

正在运行的程序就是一直在执行指令。处理器从内存中获取一条指令,对指令解码(弄清指令的语义),然后执行它。完成这条指令后处理器继续执行下一条指令,直到程序最终完成。操作系统负责让程序运行变得容易(可以运行多个程序),允许程序共享内存、和设备交互以及其他一些工作。

《Operating Systems: Three Easy Pieces》 一书中将操作系统的特征分为虚拟化、并发和持久性3个部分。

虚拟化&虚拟机

虚拟化技术是操作系统将物理资源(如CPU、RAM、Disk)转变为更通用强大的虚拟形式。因此有时候操作系统也称为虚拟机(VMvare Work Station、JVM亦是虚拟机)。操作系统会提供系统调用,像标准库一样提供给应用程序用以运行、访问内存和I/O设备等。由于虚拟化技术使得许多程序共享CPU、RAM、Disk,因此操作系统是计算机系统资源的管理者,在资源分配上达到高效公平或是更多目标。

接下来通过实际的代码看看相关虚拟技术的表现。

虚拟化CPU

先通过代码来模拟一下多进程的情形。

common.h:

#ifndef __common_h__
#define __common_h__

#include <sys/time.h>
#include <sys/stat.h>
#include <assert.h>

double GetTime() { // 获取当前时间
	struct timeval t;
	int rc = gettimeofday(&t, NULL);
	assert(rc == 0);
	return (double) t.tv_sec + (double) t.tv_usec/1e6;
}


void Spin(int howlong) { // 等待howlong
	double t = GetTime();
	while ((GetTime() - t) < (double) howlong)
		;
}

#endif

cpu.c:

间隔1s钟反复打印命令行传入的参数(字符串)。

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.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) {
		Spin(1);
		printf("%s\n", str);
	}
	return 0;
}

下面我们在Linux下运行这段程序,使用的shell为tcsh。

这里的shell命令中,&用于创建后台进程。执行命令后,可以看到操作系统创建了PID(Process Identifier,进程标识符)为2086、2087、2088、2089的4个进程,虽然只有一个处理器,但4个进程似乎是在同时运行。这是因为操作系统提供了一种假象,即系统中拥有无限多的CPU,从而使得许多程序在宏观上看起来像是同时在运行,这就是虚拟化CPU

1次运行4个进程会引发很多问题,这也是操作系统课程的聚焦。在这里的运行结果中我们可以看到,ABCD字符并没有按照顺序输出。程序的运行时间由操作系统的调度策略来决定。

如果要将程序停止,使用kill命令通知操作系统即可。

虚拟化内存

下面尝试同时运行多个操作内存的程序,看看运行时内存中值的情况。

mem.c:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char *argv[]) {
	int *p = malloc(sizeof(int));
	assert(p != NULL);
	printf("(%d) memory address of p: %o8x\n", 
           getpid(), (unsigned) p);  // 打印分配的内存地址
	*p = 0;    // p指向地址存入0
	while (1) {
		Spin(1);
		*p = *p + 1;
		printf("(%d) p: %d\n", getpid(), *p);
	}

	return 0;
}

在这里需要关闭ASLR(空间地址随机化布局),从而使程序分配到的内存段起始地址保持一致。

setarch $(uname --machine) --addr-no-randomize /bin/bash

运行多个内存分配程序,可以观察到每个程序都在相同的地址(555592a0)处分配了内存。对于该地址内存单元存储的值,每个程序都能够独立的更新而不受其他程序影响。在程序看来,内存是其私有的,而不是与其他正在运行的程序共享物理内存。

这就是虚拟化内存技术,每个进程访问自己的私有虚拟地址空间(virtual address space),然后操作系统以某种方式将虚拟内存映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。 对于正在运行的程序,它完全拥有自己的物理内存。

并发(Concurrency)

在上面关于虚拟化的实验中,可以看到操作系统同时处理很多事情,首先运行一个进程,然后再运行一个进程,以此类推。但是运行结果表明,这样做会导致一些深刻而有趣的问题。

我们看看linux c下的一个多线程程序 threads.c:

#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);
	// 创建两个线程,每个线程调用worker()增加共享计数器的值
    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;
}

这里要注意使用gcc编译时需要将线程库引入:

这里的运行结果和预期一致。但是我们加大loops数值,异常就会逐步显现出来:

这样不寻常的结果与指令如何执行有关。在面的程序中,关键部分是增加共享计数器的地方,它需要 3 条指令:一条将计数器的值从内存加载到寄存器,一条将其递增,操一条将其保存回内存。 但这3条指令并非原子方式执行(atomically,所有指令一次性执行),因此会导致异常。

持久性

RAM中的内存断电即丢失,因此需要硬件和软件持久的存储数据。Hard drive 和 SSD 是典型的用于存储长期保存的数据的IO设备。操作系统为CPU和内存提供了抽象,但是并不会为每个应用创建虚拟磁盘,并假设用户经常需要资源共享

下面看一个学习C语言文件操作时常见的例子: io.c

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>

int main(int argc, char *argv[]) {
	int fd = open("/tmp/file", 
                  O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
	assert(fd > -1);
	int rc = write(fd, "Hello World\n", 13);
	assert(rc == 13);
	close(fd);
	
	return 0;
}

在这里程序向操作系统发出了open()write()close() 3个调用。这些系统调用(system call)被转到文件系统(file system)的操作系统部分,然后该系统处理这些请求。中间操作系统为了写入数据究竟做了什么,这些情况目前是透明的。

设计理念&目标

操作系统中基本思想方法是通过建立一些抽象(abstraction),屏蔽一些底层细节,让系统方便易用。就像用C这样的高级语言编写这样的程序不用考虑汇编,用汇编写代码不用考虑逻辑门,用逻辑门来构建处理器不用太多考虑晶体管。

设计和实现操作系统的一个目标,是尽可能提供高性能(performance),也就是最小化操作系统的开销(minimize the overhead)。另一个目标是操作系统需要可信,在 OS 和应用程序之间提供保护(protection),确保某个程序的恶意行为不影响其他程序。因此进程间彼此隔离(isolation)是保护的关键。

posted @ 2021-03-05 23:52  hellcat9  阅读(283)  评论(0编辑  收藏  举报