操作系统在用户和硬件之间建立起抽象层。
- 提供公共服务
操作系统通过对于裸机的抽象向用户进程提供了诸如读取、修改文件,与其它进程通信,或等待其它进程的服务。这些服务不要求用户进程理解操作系统对服务的实现方式和裸机的运行原理(例如:用户在打开文件时不需要知道文件存储在磁盘的哪个位置以及操作系统是如何读取文件的),因此操作系统可以被视为一个友好的用户接口和公共服务的提供者。
- 协调进程交互
多个进程必须共享有限的物理资源,而这些进程并不知道自己和其它进程对于资源的实际调配。因此需要操作系统统筹规划,给每个进程分配适当的处理器时间、内存和其它资源。进程间的同步和通信、在进程切换过程中处理器和高速缓存器状态的变化、确保各个进程的正常运行等都属于操作系统的协调范围。
- 管理与控制资源
保证所有进程都可以正常运行,因此必须将有限的物理资源(处理器时间、内存、I/0设备时间)合理分配给不同进程,提高资源利用率,缩短交互进程响应时间,防止进程利用其它进程的资源或恶意干扰其运行。
内核
一般的操作系统安装盘除了带有上述功能以外往往还包括了很多别的部分,比如浏览器、文本编辑器、电子邮件服务等等。
为了区分这些功能和我们刚才定义的操作系统的功能,我们把执行操作系统的核心功能(提供公共服务、协调进程交互、构建虚拟机、管控物理资源)的部分称为操作系统内核(kernel)。不同于浏览器等程序,内核无时无刻不在计算机上运行——计算机启动时第一个运行的进程就是内核,所有用户进程都基于内核运行,所有资源管控、进程交互协调都由内核完成。
我们可以将处理器运算的时间分为两部分:
- 一部分时间里,内核在处理器上运行,分配资源给用户进程,决定下一个运行的进程;
- 另一部分时间里,用户进程在运行。
鉴于用户进程必须通过内核获得资源,并且不能接触除自己的资源以外的资源,用户进程相对于内核拥有更少的权限和资源,而且内核必须与用户进程使用不同的存储空间,防止用户进程获得内核的信息。
系统空间和用户空间
将内核所用的存储空间称为系统空间(kernel space),用户所有的空间称为用户空间(user space)。
双模式操作、用户态、内核态
我们用两种不同的模式来区分用户与内核的不同权限,这种区分被称为双模式操作(dual mode operation)。两种模式由处理器中的一个位区分,当处理器要执行某些只允许在内核态执行的特权操作时,它会先通过这个位判断当前是否处于内核态,如果一个进程企图越权操作,处理器就会触发异常(我们将在后续章节中对异常做出更多解释),使进程被内核终止。
但另一方面,操作系统是公共服务提供者——如果其它进程完全不能接触内核,那么它们也就无法使用内核提供的诸如读写文件、进程间通信等服务。为了在提供服务的同时确保安全,操作系统提供了一套给用户进程的服务,即系统调用,使用户进程可以在固定的位置进入系统空间,使用系统提供的服务。所有系统调用程序地址被存放在存储器某一位置,被称为系统调用表。
当然,恶意用户进程仍然可以通过使用空指针、超出缓冲区大小范围等办法对内核进行攻击,因此在设计内核时,我们必须在实现系统调用时检查用户空间指针的合理性后再将参数复制到系统空间,进行操作。
中断和异常
系统调用是计算机从用户态进入内核态的一种方法,其它方法还包括中断(由进程外部弓|起的)与异常。中断与异常又分别称为外中断与内中断。
异常(exception),即内中断或同步中断,是在进程运行过程中来自处理器内部的中断信号。这些中断信号可能源于程序的非法操作(如除数为 0、超长度读取数组等)、硬件故障等,而它们的中断信号将弓|起内核中对应的异常处理程序处理。
与异常对应的是外中断(interrupt)或异步中断,即来自处理器之外的中断信号。外中断包括时钟中断(即一个进程用完一定的处理器时间后,时钟会发出中断信号,使计算机进入内核态,决定下一个运行的进程)、设备I/0中断。这些中断信号 都会引发计算机切换到内核态,处理中断信号。
如果同时有多个中断发生时,我们必须先处理其中一个。因此,不同中断信号有不同的优先等级,处理器会先处理优先等级高的中断,之后再处理优先等级低的中断。如果在处理一个中断的过程中,出现了另一个优先等级更高的中断信号,那么处理器可能在完成处理这个中断信号前,切换到新的中断信号处理程序,这时我们就有了多重中断。有时为了避免中断占取过多的处理器时间,我们可以在处理中断信号的过程中屏蔽某一优先等级的中断信号或某个单独的中断信号。
与系统调用类似,x86 系统将所有进入中断服务程序的地址按照一-定顺序存储于存储器中某一位置,称为中断向量表(interrupt vector)。这一向量表控制了用户进程进入系统的地址,保护内核不被篡改。
进入内核的示例
#include <stdio.h>
/* YOUR CODE HERE */
#include <stdlib.h>
char* find_my_mood() {
char* my_mood;
/* YOUR CODE HERE */
my_mood = malloc(6*sizeof(char));
my_mood[0] = 'h';
my_mood[1] = 'a';
my_mood[2] = 'p';
my_mood[3] = 'p';
my_mood[4] = 'y';
my_mood[5] = '\0';
return my_mood;
}
int main() {
char* mood;
/* YOUR CODE HERE */
mood = find_my_mood();
printf("I am currently %s\n", mood);
/* YOUR CODE HERE */
free(mood);
return 0;
}
练习:以下代码哪里有错误
#include <stdio.h>
int main(){
char* test = "test";
test[0] = 'j';
printf("This is a %s\n", test);
char* test2 = malloc(4*sizeof(char));
realloc(test2, 21);
/* 如果你没有见过 strcpy 函数也不要担心,这一行没有问题。 */
strcpy(test2, "This is a real test\n");
printf("%s", test2);
return 0;
}
答案:
1)分配堆内存后没有释放;
2)realloc 有可能将分配的堆内存移到了与原指针所指的位置不同的地址,因此必须用 realloc 的返回值更新 test2
因为当 realloc 第一个参数所指向的内存空间大小不足够扩大为第二个参数所指定的的大小时,realloc 将新分配一段足够大的内存空间,将旧的那段内存空间里的内容拷贝过去然后释放旧的内存空间。
3)"test“存储在内存的常量区中,因此不能被修改。
实用函数
char* strcpy(char * dest, char* src);
是一个将存放在 src
里的字符串复制到 dest
的字符串里的函数;
它的返回值是 dest
。需要注意的是,strcpy
在复制字符串时会自动在末尾加上\0
,因此 dest
的长度必须比 src
多至少 1 个字符;如果 dest
的长度小于这个长度,你就会在运行时碰到段错误(Segmentation Fault)。这个看似很小的问题可能会导致你 debug 到凌晨三点,所以绝不要忘记!
但是如何才能方便的知道一个字符串的长度呢?在 C 语言程序设计里我们已经写过一个 str_len 函数,但其实 string.h已经为我们提供了这样一个函数:
size_t strlen (const char* str);
这个函数在输入的字符串 str
中寻找\0
,返回从第一个字符到第一个\0
的长度(不包括\0)。注意,如果你用一个长度为 100 的数组存储了一个实际长度只有 5 的字符串,strlen 会返回 5,而不是 100。
如果你想要将某一个字符串接到另一个字符串的后面,那么你需要应用的函数就是:
char* strcat(char* dest, char* src);
它可以将src
里存储的字符串接到dest
的后面,它的返回值也是dest
。
字符串比较:
int strcmp(char* str1, char* str2);
这个函数可以比较两个字符串是否完全一致。如果函数返回0 ,则表示两个输入的字符串完全一致;如果函数返回值大于0 ,则 str1
与str2
第一个分歧的字符处, str1
的字符比str2
的同位置字符对应更大的值(你可以参考 ASCII 表格来找到每个字符唯一对应的整数),否则str2
的第一个分歧字符大于 str1 的第一个分歧字符。
最后一个我们想要介绍的函数非常重要——在处理输入值时,我们很多时候需要用这个函数将不同的参数分隔开。这个函数就是:
char* strtok(char* str, const char* delimiters);
这个函数将输入的字符串str
用输入的分隔符 delimiters
分为更短的字符串。 delimiters
是一个含有多个字符的字符串,其中每一个字符都是一个独立的分隔符。如\n\t
中\n
和\t
分别可以作为独立的分隔符。需要注意的是,strtok
会修改输入的字符串 str
;因此如果你想保留原有的字符串,最好先
用strcpy
将原有的字符串复制到另一个字符串里,然后再将字符串输入到 strtok
里分割。当我们将一个字符串 str
输入到strtok
里后, strtok
会返回一个指向第一个由非分隔符字符的指针的分割片段;之后的每一次调用,我们都会把 NULL
作为第一个参数,如果调用成功就会返回下一个分割片段,如果已经到达 str
的末尾则会返回NULL
。
网友评论