Think OS - lichuncen/lichuncen.github.io GitHub Wiki
查看程序名称与使用的函数
# 编译全流程并制定output名称
$ gcc hello.c -o hello
# 编译,得到机器码
$ gcc hello.c -c
# UNIX命令可以读取目标文件并生成关于它所定义和所使用的名称的信息。
$ nm hello.o
# 编译,得到汇编代码
$ gcc hello.c -S
# 预处理,包括头文件等
$ gcc hello.c -E
查看当前终端的进程和所有进程
# 运行中进程的信息,PID,TTY,TIME,CMD
# TTY(Teletypewriter)电传打字机,即创建进程的终端
$ ps
# 所有用户的进程
$ ps -e
自信息:有1/n概率的事件具有log(2, n)信息
二进制单位GiB来描述主存大小,并使用十进制单位GB和TB来描述 HDD的大小。
对处理器的抽象:流水线与线程 对内存的抽象:堆栈 对磁盘的抽象:层级缓存
MMU内存管理单元执行VA和PA之间的翻译
- 当程序读写变量时,CPU会得到VA。
- MMU将VA分成两部分,称为页码和偏移。“页”是一个内存块,页的大小取决于操作系统 和硬件,通常为1~4KiB。
- MMU在“页表”里查找页码,然后获取相应的物理页码。之后它将物理页码和偏移组合得 到PA。
- PA传递给主存,用于读写指定地址。
页表是“稀疏”的,一种选择是多级页表,它被多数操作系统例如Linux所采用。另一种选择是关联表,其中每个 条目包含虚拟页码和物理页码。在软件上搜索关联表会非常慢,但是硬件上我们可以并行搜 索整个表,所以关联数组经常用于在MMU中表示页表。
“文件系统”和底层机制的根本不同,就是文件是基于字节的,而持久化储存器是基于块的。操作系统将C标准库中基于字节的文件操作翻译成基于块的储存设备操作。每个块的典型大小是 1~8KiB。
块大小与文件大小
Unix使用索引节点inode来记录块位置和文件信息,包括文件拥有者的用户ID,表明谁可以读写或执行的权限位,以及 表明最后修改和访问时间的时间戳。另外,inode包含直接指向组成文件的前12个块的指针。Inode的间接块包含了指向其它块的指针,其中指针数量取决于块的数量和大小,它通常是1024。如果有1024个块,每个块是 8KiB,那么一个间接块可以编址8MiB。二级间接块含有指向间接块的指针。我们可 以使用1024个间接块来编址8GiB。最后有一个三级间接块,它含有指向二级间接块指针,支持最大8TiB 的文件大小。
而另一些文件系统,例如FAT,使用了一张文件分配表,它为每个块包含一个 条目,在这个上下文中叫做“簇”。根目录包含指向每个文件第一个簇的指针。FAT上每个簇的 条目指向文件中的下一个簇,就像链表那样。
文件抽象实际上是“字节流”的抽象
而字节流还应用于Unix管道和网络通信。Unix管道是进程间通信的一个简单形式。可以建立这样一些进程,使一个进程的输出用作另一个进程的输入。对于第一个进程,管道表现为打开用于写入的文件,所以 它可以使用C标准库类似 fputs 和 fprintf 的函数。对于第二个进程,管道表现为打开用于读 取的文件,所以它可以使用 fgets 和 fscanf 。
网络通信也使用了字节流的抽象。Unix套接字socket是一个数据结构,它(通常)表示两个不同电脑上的进程之间的信道。同样,进程可以使用“文件”处理函数从套接字读取数据和向套接字写 入数据。
符号扩展……将符号位复制到新的位上,比如8位转16位的补码表示,对unsigned不适用。
&清除位,得掩码;|设置位;^反转位。
# 在C中对浮点数打包和解包
union {
float f;
unsigned int u;
} p;
p.f = -13.0;
unsigned int sign = (p.u >> 31) & 1;
unsigned int exp = (p.u >> 23) & 0xff;
unsigned int coef_mask = (1 << 23) - 1;
unsigned int coef = p.u & coef_mask;
printf("%d\n", sign);
printf("%d\n", exp);
printf("0x%x\n", coef);
C提供了4种用于动态内存分配的函数:
-
malloc ,它接受表示字节单位的大小的整数,返回指向新分配的、(至少)为指定大小 的内存块的指针。如果不能满足要求,它会返回特殊的值为 NULL 的指针。
-
calloc ,它和 malloc 一样,除了它会清空新分配的空间。也就是说,它会设置块中所 有字节为0。
-
free ,它接受指向之前分配的内存块的指针,并会释放它。也就是说,使这块空间可用 于未来的分配。
-
realloc ,它接受指向之前分配的内存块的指针,和一个新的大小。它使用新的大小来 分配内存块,将旧内存块中的数据复制到新内存块中,释放旧内存块,并返回指向新内 存块的指针。
这套API是出了名的易错和苛刻。
常在安全的内存管理和性能之间有个权衡。例如,内存错误的的最普遍来源是数组的越界写入。这一问题的最显然的解决方法就是边界检查。也就是说,每次对数组的访问都应该检查下标是否越界。提供数组结构的高阶库通常会进行边界检查。但是C风格数据和大多数底层库不会这样做。
如果 malloc 返回了 NULL ,但是你仍旧把它当成分配的内存块进行访问,你会得到段错误。 因此,在使用之前检查 malloc 的结果是个很好的习惯。一种选择是在每个 malloc 调用之后 添加一个条件判断。
迭代数组
读写元素并度量平均时间的程序。通过改变数组的大小,就 有可能推测出缓存的大小,块的大小,和一些其它属性。
# cache.c
iters = 0;
do {
sec0 = get_seconds();
# for循环遍历了数组。limit决定数组遍历的范围。stride决定跳过多少元素。
for (index = 0; index < limit; index += stride)
array[index] = array[index] + 1;
iters = iters + 1;
sec = sec + (get_seconds() - sec0);
} while (sec < 0.1);
如果数组比缓存大小更小,或步长小于块的大小,我们认为会有良好的缓存性能。如 果数组大于缓存大小,并且步长较大时,性能只会下降。
如果你处理二维数组,它以行数组的形式储存。如果你需要遍历元素,按行遍历并且步长为 元素大小会比按列遍历并且步长为行的大小更快。
类似归并排序的递归策略通常具有良好的缓存行为,因为它们将大数组划分为小片段,之后 处理这些小片段。有时这些算法可以调优来利用缓存行为。
缓存感知:设计适配缓存大小、块大小以及其它硬件特征的算法。
缓存策略
存储器层次结构展示了一个考虑到缓存的框架。在结构的每一级中,我们都需要强调四个缓存的基本问题:
- 谁在层次结构中上移或下移数据?在结构的顶端,寄存器通常由编译器完成分配。CPU 上的硬件管理内存的缓存。在执行程序或打开文件的过程中,用户可以将存储器上的文件隐式移动到内存中。但是操作系统也会将数据从内存移动回存储器。在层次结构的底 端,管理员在磁带和磁盘之间显式移动数据。
- 移动了什么东西?通常,在结构顶端的块大小比底端要小。在内存的缓存中,通常块大小为128B。内存中的页面可能为4KiB,但是当操作系统从磁盘读取文件时,它可能会一次读10或100个块。
- 数据什么时候会移动?在多数的基本的缓存中,数据在首次使用时会移到缓存。但是许多缓存使用一些“预取”机制,也就是说数据会在显式请求之前加载。我们已经见过预取的 一些形式了:在请求其一部分时加载整个块。
- 缓存中数据在什么地方?当缓存填满之后,我们不把一些东西扔掉就不可能放进一些东西。理想化来说,我们打算保留将要用到的数据,并替换掉不会用到的数据。
这些问题的答案构成了“缓存策略”。
页面调度/换页:在带有虚拟内存的系统中,操作系统可以将页面在存储器和内存之间移动。
当进程A运行时,它会收回进程B所需的页面,之后进程B运行时,它又会收回进程A 所需的页面。当这种情况发生时,两个进程都会执行缓慢,系统会变得无法响应。这种我们不想看到的场景叫做“颠簸”。
理论上,操作系统应该通过检测调度和块上的增长来避免颠簸,或者杀掉进程直到系统能够再次响应。但是在我看来,多数系统都没有这样做,或者做得不好。它们通常让用户去限制 物理内存的使用,或者尝试在颠簸发生时恢复。
Shell
究其本质,内核的工作就是处理中断。“中断”是一个事件,它会停止通常的指令周期,并且使 执行流跳到称为“中断处理器”的特殊代码区域内。
当一个设备向CPU发送信号时,会发生硬件中断。例如,网络设备可能在数据包到达时会产生中断,或者磁盘驱动器会在数据传送完成时产生中断。多数系统也带有以固定周期产生中断的计时器。
软件中断由运行中的程序所产生。例如,如果一条指令由于某种原因没有完成,可能就会触 发中断,便于这种情况可被操作系统处理。一些浮点数的错误,例如除零错误,会由中断处 理。
当程序需要访问硬件设备时,会进行“系统调用”,它就像函数调用,除了并非跳到函数的起始位置,而是执行一条特殊的指令来触发中断,使执行流跳到内核中。内核读取系统调用的参 数,执行所请求的操作,之后使被中断进程恢复运行。
在工作站或笔记本上,调度器的首要目标就是最小化响应时间,也就是说,计算机应该快速 响应用户的操作。响应时间在服务器上也很重要,但是调度器同时也可能尝试最大化吞吐量,它是单位时间内所完成的请求。系统调用 nice 允许进程降低(但不能升高)自己的优先级,并允许程序员向调度器传递显式的信息。
PThread
C语言使用的所普遍的线程标准就是POSIX线程,简写为 pthread 。POSIX标准定义了线程模型和用于创建和控制线程的接口。多数UNIX的版本提供了POSIX的实现。C11标准也提供了POSIX线程的实现。为了避免冲突,函数的前缀改为了 thrd 。
# 头文件应该包括 <pthread.h>
# 在gcc中和pthread一起编译,命令行中使用-l选项
$ gcc -g -O2 -o array array.c -lpthread
啊可恶、互斥mutex、队列queue、条件变量condition、生产者-消费者
信号量semaphore,但居然,可以用条件变量和互斥体实现信号量。
译者有趣且给力哈哈哈