有任何错误烦请指出,共同学习成长,谢谢。
《Operating Systems: Three Easy Pieces》的组织为三大部分(现在新增有安全的部分):
- Virtualization(虚拟化?很多东西都是虚的,肯定有其存在的作用,比如虚拟内存,虚拟的核心?);
- Concurrency(并发,处理多任务的能力,但是不一定同时执行。比如单核情况下的任务切换,尽管在CPU是单核的情况下,但是让人感觉在同时处理多个任务,比如网页和音乐“同时”运行。);
- Persistence(持久性,物理内存上的数据会因为通电而刷新,如何把这部分易失的数据保存起来,这就涉及到硬盘等存储介质)。
引入:
程序怎么运行?执行指令(instructions)。处理器从内存里取到(fetch)指令,解码(decode:知道这个指令是什么),然后执行。重复上述的若干过程,直到程序最终结束。
有一种软件体负责让程序运行得更容易(比如:允许程序共享内存,允许程序和设备交互),这就是Operating System(操作系统)。
怎么使得系统操作的结果正确且高效?
- 虚拟化:把物理资源(physical resource,比如处理器,内存,硬盘)转成易于使用的虚拟形式。操作系统有时也被叫虚拟机(virtual machine),为了使用户充分利用OS的功能(运行程序,分配内存,访问文件等),OS会提供一些可以调用(call)的接口(APIs)。有时也称OS为应用提供了标准库(standard library)。
虚拟化允许程序运行(共享CPU),并发访问各自的指令和数据(共享内存),还有访问设备(比如硬盘),所以OS有时也被认为是资源管理者。
1.1. 虚拟化cpu
不妨在Ubuntu下尝试一下程序:
cpu.c
#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;
}
其中,Spin(t)是等待t秒后继续运行。
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) {
double t = GetTime();
while ((GetTime() - t) < (double) howlong)
; // do nothing in loop
}
#endif // __common_h__
在终端(Terminal)执行
gcc -o cpu cpu.c
./cpu a & ./cpu b & ./cpu c & ./cpu d &
运行会得到类似a b d c的循环输出,好像是程序在同时运行,为何?
在硬件的帮助下,OS控制着这种假象(好像CPU有很多虚拟核心的假象)。
当然,为了运行和停止程序,这里面会有很多API涉及到如何去提交需求给OS。
同时运行程序会涉及到不少的问题,比如:在同一时刻,先运行哪一个程序?(为什么上面的程序没有按照a b c d的顺序输出?)这需要考虑OS的策略(policy)。
1.2. 虚拟化内存(Memory)
物理内存模型在现代机器的表现很简单。就是一个字节数组。访问内存要知道数据的地址。程序的数据和程序本身的指令都在内存。
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) address pointed to by p: %p\n",getpid(),p);
*p = 0;
while(1){
Spin(1);
*p = *p + 1;
printf("%d) p : %d \n",getpid(), *p);
}
return 0;
}
运行结果:
./mem0
(21843) address pointed to by p: 0x56499b9f2260
21843) p : 1
21843) p : 2
21843) p : 3
21843) p : 4
21843) p : 5
21843) p : 6
注意:此处运行时应关闭地址空间的随机化(address-space randomization)
具体的步骤为终端进入root后使用 echo 0 > /proc/sys/kernel/randomize_va_space 指令即可。
./mem0 & ./mem0 &
(24968) address pointed to by p: 0x555555756260
(24969) address pointed to by p: 0x555555756260
24968) p : 1
24969) p : 1
24968) p : 2
24969) p : 2
24968) p : 3
24969) p : 3
24968) p : 4
24969) p : 4
为什么同一个指针,但是数据却一样的增长?这就是内存的虚拟化。每个进程有各自私有的虚拟地址空间,这个空间映射到机器的物理内存上。
1.3. 并发
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);
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;
}
common_threads.h
#ifndef __common_threads_h__
#define __common_threads_h__
#include <pthread.h>
#include <assert.h>
#include <sched.h>
#ifdef __linux__
#include <semaphore.h>
#endif
#define Pthread_create(thread, attr, start_routine, arg) assert(pthread_create(thread, attr, start_routine, arg) == 0);
#define Pthread_join(thread, value_ptr) assert(pthread_join(thread, value_ptr) == 0);
#define Pthread_mutex_lock(m) assert(pthread_mutex_lock(m) == 0);
#define Pthread_mutex_unlock(m) assert(pthread_mutex_unlock(m) == 0);
#define Pthread_cond_signal(cond) assert(pthread_cond_signal(cond) == 0);
#define Pthread_cond_wait(cond, mutex) assert(pthread_cond_wait(cond, mutex) == 0);
#define Mutex_init(m) assert(pthread_mutex_init(m, NULL) == 0);
#define Mutex_lock(m) assert(pthread_mutex_lock(m) == 0);
#define Mutex_unlock(m) assert(pthread_mutex_unlock(m) == 0);
#define Cond_init(cond) assert(pthread_cond_init(cond, NULL) == 0);
#define Cond_signal(cond) assert(pthread_cond_signal(cond) == 0);
#define Cond_wait(cond, mutex) assert(pthread_cond_wait(cond, mutex) == 0);
#ifdef __linux__
#define Sem_init(sem, value) assert(sem_init(sem, 0, value) == 0);
#define Sem_wait(sem) assert(sem_wait(sem) == 0);
#define Sem_post(sem) assert(sem_post(sem) == 0);
#endif // __linux__
#endif // __common_threads_h__
同时运行程序带来了一些有趣的问题,这些问题不止是OS上的,现代的多线程程序也会存在问题。
上面程序创建了两个线程(可以认为是运行在同样内存空间的函数),然后执行worker(),增加counter的值。
gcc -o threads threads.c -pthread
./threads 1000
Initial value : 0
Final value : 2000
没错,在上面的运行结果中,我们期待worker里的循环为N次的时候,counter增加N,那么两个线程应该增加2N。如果N的数值加大呢?
./threads 100000
Initial value : 0
Final value : 111818
./threads 100000
Initial value : 0
Final value : 124142
第一次和第二次都得到了不同的数值,而且是错误的数值。有可能不断的运行,得到一个正确的结果,为什么会这样?
主要是因为这个操作不是原子(atomic,也就是一次执行完指令)的。counter增加的操作分三步:将counter数值从内存加载到寄存器,增量,然后把数值存回内存。
那么,当许多线程并发执行,如何正确运行,OS需要提供什么原语(primitives)操作?硬件需要提供什么机制?我们怎么才能解决并发的问题?
在系统内存中,数据是容易丢失的,因为DRAM以易变的方式存储。当系统崩溃或者断电,内存上的数据的丢失了。因此,我们想要持久的存储数据。I/O设备中就有硬盘(hard drive)满足这种需求。不止机械硬盘(hdd, short for hard-disk drive),还有固态硬盘(ssd, short for solid-state drive)。
操作系统中,管理硬盘的软件是文件系统(file system)。OS不提供像CPU和内存的抽象,而是假定用户要分享文件内的信息。举个例子,使用文本编辑器写个名为main.c程序,然后使用gcc将其转成可执行文件main,然后使用./运行它。
为了更好的理解,看看例子。
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;
}
程序中,创建文件(/tmp/file),保存"hello world"。使用了三个系统调用(calls),第一个是open(),打开文件并创建它;第二个是write(),写进数据;第三个是close(),关闭文件,程序不会再写入数据了。这些调用都被导(routed)向文件系统,文件系统处理请求并返回某种错误码。
gcc -o io io.c
./io
cat /tmp/file
hello world //结果
文件系统要做很多事情来写数据到硬盘上:首先决定把这个数据放在硬盘的哪个位置,然后在各种文件系统的结构中跟踪它。这就需要将I/O请求传到底层的存储设备,来读或更新(写)它们。设备驱动(device driver)是OS知道如何与特定设备打交道的代码。自己写的话相当复杂,OS提供了系统调用(system calls)来访问设备,因此,OS有时也被看作标准库(standard library)。
为了性能, 大部分文件系统会延迟写操作,希望有一大批写操作之后再执行。
为了应对写操作可能的崩溃,有一些复杂的写协议(protocol),比如journaling或者copy-on-write,来确保当写失败的时候,可以恢复到合理的状态。
同时,文件系统应用了许多数据结构使得操作高效,比如列表或者b-trees。后续仍有RAIDs等等的细节。
- 操作系统要提供高性能,也即最小化OS的开销。而虚拟化和让系统易用值得一部分性能,比如:时间(更多的指令)和额外的空间(内存或者存储)。
- OS还要提供应用之间的保护、OS和应用之间的保护。因为同时运行多个程序的时候不希望一个程序的坏行为影响到另一个程序、或者影响到OS。有一些机制比如隔离(isolation)可以充当一定的保护。
- 操作系统必须不停地运行,如果崩溃,所有的程序也一起崩溃。OS争取提供更高的可靠性(reliability)。
- 另一些目标是高能效(日益增加的)、安全性(抵抗恶意应用)、移动性(运行在越来越小的设备上),这些目标取决于使用什么样的设备。
可惜的是,本书不对网络(networking)和图形(graphics)设备以及安全性(security)做深入的内容。
网友评论