《Operating Systems Three Easy Pieces》 操作系统导论【一】 虚拟化 CPU_操作系统three piece(1)

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

prompt> ./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &
[1] 7353
[2] 7354
[3] 7355
[4] 7356
A
B
D
C
A
B
D
C
A
C
B
D


当同时执行运行`4` 个程序的命令时,打印几乎是同时运行的,而不是等待第一个程序运行结束才运行下个程序  
 对应单核的处理器,同时运行 `4`个进程是不可能的,所有这里就要介绍 `CPU` 的虚拟化  
 事实证明,在硬件的一些帮助下,操作系统负责提供这种假象(`illusion`),即系统拥有非常多的虚拟 `CPU` 的假象。将单个 `CPU`(或其中一小部分)转换为看似`无限数量`的 `CPU`,从而让许多程序看似同时运行,这就是所谓的`虚拟化 CPU`(`virtualizing the CPU`)  
 当然运行不同进程时的策略,如优先级等也是需要讨论的  
 知识点:时分共享,上下文切换


### 2.虚拟化内存



#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include “common.h”

int main(int argc, char *argv[])
{
int *p = malloc(sizeof(int)); // a1
assert(p != NULL);
printf(“(%d) memory address of p: %08x\n”,
getpid(), (unsigned)p); // a2
*p = 0; // a3
while (1)
{
Spin(1);
*p = *p + 1;
printf(“(%d) p: %d\n”, getpid(), *p); // a4
}
return 0;
}


这是一个访问内存的程序(`mem.c`)  
 该程序做了几件事。首先,它分配了一些内存(`a1` 行)。然后,打印出内存的地址(`a2` 行),然后将数字 `0` 放入新分配的内存的第一个空位中(`a3` 行)。最后,程序循环,延迟一秒钟并递增 `p` 中保存的值。在每个打印语句中,它还会打印出所谓的正在运行程序的进程标识符(`PID`)(`a4` 行)。该 `PID` 对每个运行进程是唯一的。  
 该程序的输出如下:



prompt> ./mem
(2134) memory address of p: 00200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
(2134) p: 4
(2134) p: 5
ˆC


* 当只运行一个程序时,p 递增,一切正常



prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) memory address of p: 00200000
(24114) memory address of p: 00200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24113) p: 3
(24114) p: 3
(24113) p: 4
(24114) p: 4


当同时运行多个相同的程序时,分配的内存地址竟然是相同的,先抛开虚拟化的概念,以物理内存的角度看待,这几个程序分配的内存指针指向了同一块内存空间,也就是修改其中一个程序修改内存也会导致另一个程序中的值改变  
 但是从结果来看这两块内存相互独立,并不影响,就好像每个正在运行的程序都有自己的私有内存,而不是与其他正在运行的程序共享相同的物理内存  
 实际上,这正是操作系统虚拟化内存(`virtualizing memory`)时发生的情况。每个进程访问自己的私有虚拟地址空间(`virtual address space`)(有时称为地址空间,`address space`),操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。但实际情况是,物理内存是由操作系统管理的共享资源。  
 知识点:(等待补充)


### 3.并发



#include <stdio.h>
#include <stdlib.h>
#include “common.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 \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()`创建了两个线程(`thread`),每个线程中循环了 `loops` 次来递增全局变量`counter`。  
 理想情况下,`counter` 最终的值应该为 `2xloops`,因为两个线程各把 `counter` 递增了 `loops` 次



prompt> ./thread 100000
Initial value : 0
Final value : 143012 // huh??
prompt> ./thread 100000
Initial value : 0
Final value : 137298 // what the??


当运行时,发现值每次各不相同,且小于 `2xloops`。  
 事实证明,这些奇怪的、不寻常的结果与指令如何执行有关,指令每一执行一条。遗憾的是,上面的程序中的关键部分是`增加共享计数器`的地方,它需要 `3` 条指令:


* 一条将计数器的值从内存加载到寄存器
* 一条将其递增
* 一条将其保存回内存。


因为这`3`条指令甚不是以原子方式(`atomically`)执行(所有的指令一一性执行)的,所以奇怪的事情可能会发生。  
 知识点: `原子操作` 。


### 4.持久性


操作系统中管理磁盘的软件通常称为文件系统(`file system`)。因此它负责以可靠和高效的方式,将用户创建的任何文件(`file`)存储在系统的磁盘上。



#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;
}


为了完成这个任务,该程序向操作系统发出`3` 个调用。第一个是对 `open()`的调用,它打开文件并创建它。第二个是 `write()`,将一些数据写入文件。第三个是 `close()`,只是简单地关闭文件,从而表明程序不会再向它写入更多的数据。这些系统调用(`system call`)被转到称为文件系统(`file system`)的操作系统部分,然后该系统处理这些请求,并向用户返回某种错误代码。  
 首先确定新数据将驻留在磁盘上的哪个位置,然后在文件系统所维护的各种结构中对其进行记录。这样做需要向底层存储设备发出`I/O`请求,以读取现有结构或更新(写入)它们。所有写过设备驱动程序(`device driver`)的人都知道,让设备现表你执行某项操作是一个复杂而详细的过程。它需要深入了解低级别设备接口及其确切的语义。幸运的是,操作系统提供了一种通过系统调用来访问设备的标准和简单的方法。因此,`OS`有时被视为标准库(`standard library`)。  
 出于性能方面的原因,大多数文件系统首先会`延迟`这些写操作一段时间,希望将其`批量分组`为较大的组。为了处理写入期间系统崩溃的问题,大多数文件系统都包含某种复杂的写入协议,如日志(`journaling`)或写时复制(`copy-on-write`),`仔细`排序写入磁盘的操作,以确保如果在`写入序列期间`发生故障,系统可以在之后恢复到合理的状态。为了使不同的通用操作更高效,文件系统采用了许多不同的数据结构和访问方法,从简单的列表到复杂的 `B 树`。


### 5.设计目标


* 抽象(`abstraction`),让系统方便和易于使用。抽象对我们在计算机科学中做的每件事都很有帮助。抽象使得编写一个大型程序成为可能,将其划分为小而且容易理解的部分
* 高性能(`performance`)。换言之,我们的目标是最小化操作系统的开销(`minimize the overhead`)。但是虚拟化的设计是为了易于使用,无形之中会增大开销,比如虚拟页的切换,cpu 的调度等等,所以尽可能的保持易用性与性能的平衡至关重要
* 应用程序之间以及在 OS 和应用程序之间提供保护(`protection`)。因为我们希望让许多程序同时运行,所以要确保一个程序的恶意或偶然的不良行为不会损害其他程序。保护是操作系统基本原理之一的核心,这就是隔离(`isolation`)。让进程彼此隔离是保护的关键,因此决定了 OS 必须执行的大部分任务
* 操作系统也必须不间断运行。当它失效时,系统上运行的所有应用程序也会失效。由于这种`依赖性`,操作系统往往力求提供高度的可靠性(`reliability`)


## (二) 抽象 : 进程


### 1.什么是进程 ?


* 操作系统为正在运行的程序提供的抽象
* 进程可以访问的内存(称为地址空间,`address space`) 是该进程的一部分。
* 进程的机器状态的另一部分是寄存器。
* 例如,程序计数器(`Program Counter`,PC)(有时称为指令指针,Instruction Pointer 或 IP)告诉我们程序当前 正在执行哪个指令;类似地,栈指针(`stack pointer`)和相关的帧指针(`frame pointer`)用于 管理函数参数栈、局部变量和返回地址。


### 2.进程API :


\ 创建(`create`):操作系统必须包含一些创建新进程的方法。在 shell 中键入命令 或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。  
 \ 销毁(`destroy`):由于存在创建进程的接口,因此系统还提供了一个强制销毁进 程的接口。当然,很多进程会在运行完成后自行退出。但是,如果它们不退出, 用户可能希望终止它们,因此停止失控进程的接口非常有用。  
 \ 等待(`wait`):有时等待进程停止运行是有用的,因此经常提供某种等待接口。 \ 其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他4.3 进程创建:更多细节 21  
 控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间), 然后恢复(继续运行)。  
 \ 状态(`statu`):通常也有一些接口可以获得有关进程的状态信息,例如运行了多 长时间,或者处于什么状态。  
 \ 其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间), 然后恢复(继续运行)


### 3. 进程创建:更多细节


* 操作系统如何启动并运 行一个程序?进程创建实际如何进行 ?  
 ![](https://img-blog.csdnimg.cn/bb275cc7d47643c4a5f58335b0524bb6.png#id=pNqo2&originHeight=1112&originWidth=2866&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
* 为栈分配空间 : 将代码和静态数据加载到内存后,必须为程序的运行时栈(`run-time stack` 或 `stack`)分配一些内存。`C 程序`使用栈存放局部变量、函数参数和返回地址。操作系统也可能会用参数初始化栈。具体来说,它会将参数填入 `main( )`函数,即 `argc` 和 `argv` 数组。
* 为堆分配空间 : 操作系统也可能为程序的堆(`heap`)分配一些内存。程序通过调用 malloc()来请求这样的空间,并通过调用 free()来明确地释放它。数据结构(如链表、散列表、树和其他有趣的数据结构)需要堆。
* `I/O` 初始化:操作系统还将执行一些其他初始化任务,特别是与输入/输出(`I/O`)相关的任务。例如,在 `UNIX` 系统中,默认情况下每个进程都有 3 个打开的`文件描述符`(`file descriptor`),用于标准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。
* 运行程序入口:通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与`I/O` 设置相关的其他工作,完成准备后,接下来就是启动程序,在入口处运行,即 `main()`。


### 4 进程状态


\ 运行(`running`):在运行状态下,进程正在处理器上运行。这意味着它正在执行 指令。  
 \ 就绪(`ready`):在就绪状态下,进程已准备好运行,但由于某种原因,操作系统 选择不在此时运行。  
 \ 阻塞(`blocked`):在阻塞状态下,一个进程执行了某种操作,直到发生其他事件 时才会准备运行。一个常见的例子是,当进程向磁盘发起 I/O 请求时,它会被阻塞, 因此其他进程可以使用处理器。  
 ![](https://img-blog.csdnimg.cn/88823457456e4199b57d9a0247d3d5a6.png#height=318&id=kYY33&originHeight=497&originWidth=676&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=433)


* 有IO 会造成进程的堵塞  
 ![](https://img-blog.csdnimg.cn/92cdc73d05f44e329e960ae86cff54fc.png#height=403&id=B8dvk&originHeight=1430&originWidth=2454&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=692)
* 操作系统必须作出许多决定来让CPU繁忙来繁忙来提高资源利用率。


### 5 数据结构



> 
> 为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种`进程列表`(process list),以及跟踪当前正在运行的进程的一些附加信息。操作系统还必须以某种方式跟踪被阻塞的进程。当 `I/O 事件完成`时,操作系统应确保唤醒正确的进程,让它准备好再次运行。
> 
> 
> 



// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context
{
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
// 可以看到实际操作系统对于进程状态的定义远不止上面介绍的3种
enum proc_state
{
UNUSED,
EMBRYO,
SLEEPING,
RUNNABLE,
RUNNING,
ZOMBIE
};
// the information xv6 tracks about each process
// including its register context and state
struct proc
{
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};


* 对于停止的进程,寄存器上下文将保存其寄存器的内容。


除了`运行、就绪和阻`塞之外,还有其他一些进程可以处于的状态:


* `初始(initial)`状态有时候系统会有一个`初始(initial)`状态,表示进程在创建时处于的状态。
* `最终(final)`状态另外,一个进程可以处于已退出但尚未清理的`最终(final`)状态(在基于 `UNIX` 的系统中,这称为`僵尸状态`)。这个最终状态非常有用,因为它允许其他进程(通常是创建进程的父进程)检查进程的返回代码,并查看刚刚完成的进程是否成功执行(通常,在基于 `UNIX` 的系统中,程序成功完成任务时返回零,否则返回非零)。完成后,父进程将进行最后一次调用(例如,`wait()`),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构


## (三)插叙:进程 API


### 1.fork()系统调用


系统调用fork()用于创建新进程



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

int main(int argc, char *argv[])
{
printf(“hello world (pid:%d)\n”, (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
} else if (rc == 0) {
// child (new process)
printf(“hello, I am child (pid:%d)\n”, (int) getpid());
} else {
// parent goes down this path (original process)
printf(“hello, I am parent of %d (pid:%d)\n”,
rc, (int) getpid());
}
return 0;
}



prompt> ./p1
hello world (pid:29146)
hello, I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)
prompt>


* 子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的 `地址空间(即拥有自己的私有内存)`、`寄存器`、`程序计数器`等,但是它从 `fork()`返回的值是不同的。父进程获得的返回值是新创建子进程的 `PID`,而子进程获得的返回值是 `0`。
* 在其他情况下,子进程可能先运行 , 会有不同的情况 , 取决于`CPU`调度


### 2.wait()系统调用


* wait()函数用于使父进程(也就是调用 `wait()`的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
printf(“hello world (pid:%d)\n”, (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
} else if (rc == 0) {
// child (new process)
printf(“hello, I am child (pid:%d)\n”, (int) getpid());
sleep(1);
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
printf(“hello, I am parent of %d (wc:%d) (pid:%d)\n”,
rc, wc, (int) getpid());
}
return 0;
}


![](https://img-blog.csdnimg.cn/2922e0d068144fa7a8a50b9fd19a9b81.png#id=u7Uz0&originHeight=236&originWidth=1172&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)


* 该系统调用会谁子进程运行结束后才返回①。因此,即使父进程先运 行,它也会礼貌地等待子进程运行完毕,然后 wait()返回,接着父进程才输出自己的信息。


### 3. exec()系统调用


* exec()这个系统调用可以让子进程执行与父进程不同的程序
* exec()有几种变体:execl()、execle()、execlp()、execv()和 execvp()。请阅读 man 手册以了解更多信息。
* [Linux多任务编程(三)—exec函数族及其基础实验\_玖零大壮的博客-CSDN博客](https://bbs.csdn.net/topics/618668825)



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
printf(“hello world (pid:%d)\n”, (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
} else if (rc == 0) {
// child (new process)
printf(“hello, I am child (pid:%d)\n”, (int) getpid());
char *myargs[3];
myargs[0] = strdup(“wc”); // program: “wc” (word count)
myargs[1] = strdup(“p3.c”); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
printf(“this shouldn’t print out”);
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
printf(“hello, I am parent of %d (wc:%d) (pid:%d)\n”,
rc, wc, (int) getpid());
}
return 0;
}


* 用 fork()、wait( ) 和 exec( )(p3.c)



prompt> ./p3
hello world (pid:29383)
hello, I am child (pid:29384)
29 107 1030 p3.c
hello, I am parent of 29384 (wc:29384) (pid:29383)
prompt>


子进程调用 execvp()来运行字符计数程序 `wc`。实际上,它针对源代码文件 p3.c 运行 wc,从而告诉我们该文件有多少行、多少单词,以及多少字节。  
 给定可执行程序的名称(如 `wc`)及需要的参数(如 `p3.c`)后,`exec()`会从可 执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过 `argv` 传递给该进程。因此,它并没有创建新进程,而是直接将当前运行的程序(以前的 `p3`)替换为不同的运行程序(`wc`)。子进程执行 `exec()`之后,几乎就像 `p3.c` 从未运行过一样。对 `exec()`的成功调用永远不会返回。如果 `exec` 函数执行失败, 它会返回失败的信息, 而且进程继续执行后面的代码。  
 注意:此时子进程的 `pid` 号并没有变,且还是该父进程的子进程,所以并不会影响 `wait( )`操作,等待该进程的操作(统计字节)完成后,wait()才会返回,父进程同时退出阻塞状态


### 


### 


### 4.为什么这样设计 API。


![](https://img-blog.csdnimg.cn/1c31a11793cc4bd9afc1c37f567cd1ac.png#id=Z0sxM&originHeight=240&originWidth=1404&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)


* fork()和 exec()的分离,让 shell 可以方便地实现很多有用的功能。



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
} else if (rc == 0) {
// child: redirect standard output to a file
close(STDOUT_FILENO);
open(“./p4.output”, O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
// now exec “wc”…
char *myargs[3];
myargs[0] = strdup(“wc”); // program: “wc” (word count)
myargs[1] = strdup(“p4.c”); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
assert(wc >= 0);
}
return 0;
}



prompt> wc p3.c > newfile.txt
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c

– p4 我实调用了 fork 来创建新的子进程,之后调用 execvp()来执行 wc。
– 屏幕上谁有看到输出, 是由于结果被重我向到文件 p4.output。


要看懂上面的例子,首先要补充点Unix文件描述符的知识  
 ![image.png](https://img-blog.csdnimg.cn/img_convert/ecd8d2b0d274f6962063f0c6b8fff984.png)


### 作业


1. 编写一个调用 `fork()`的程序。在调用 `fork()`之前,让主进程访问一个变量(例如 x)并将其值设置为某个值(例如 100)。子进程中的变量有什么值?当子进程和父进程都改变 x 的值时,变量会发生什么?


答:父进程在 fork 之前修改的值会同步到子进程中(fork 前子进程并不存在),当 fork 完成后,两个进程相互独立,修改 fork 前定义的变量时也是独立的。



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

int main(int argc, char *argv[])
{
int x = 1;
printf(“hello world (pid:%d)\n”, (int)getpid());
x = 3;
int rc = fork();
if (rc < 0)
{ // fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
}
else if (rc == 0)
{ // child (new process)
x=4;
printf(“hello, I am child (pid:%d),x:%d\n”, (int)getpid(),x);
}
else
{ // parent goes down this path (main)
wait(NULL);
printf(“hello, I am parent of %d (pid:%d),x:%d\n”,
rc, (int)getpid(),x);
}
return 0;
}



root@hjk:~/repo/os_test# ./a.out
hello world (pid:17699)
hello, I am child (pid:17700),x:4
hello, I am parent of 17700 (pid:17699),x:3


2.编写一个打开文件的程序(使用 open()系统调用),然后调用 fork()创建一个新进程。子进程和父进程都可以访问 open()返回的文件描述符吗?当它们并发(即同时)写入文件时,会发生什么?  
 答:都可以访问。并发时无影响。



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
close(STDOUT_FILENO);
int fd = open(“./p4.output”, O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
int rc = fork();
if (rc < 0)
{ // fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
}
else if (rc == 0)
{ // child: redirect standard output to a file
// now exec “wc”…
printf(“child\n”);
}
else
{ // parent goes down this path (main)
// int wc = wait(NULL);
printf(“father\n”);
}
// if(fd>=0)
// {
// close(fd);
// }
return 0;
}

p4.output 文件输出如下:
father
child


3.使用 fork()编写另一个程序。子进程应打印“hello”,父进程应打印“goodbye”。你应该尝试确保子进程始终先打印。你能否不在父进程调用 wait()而做到这一点呢?  
 答:使用 sleep 函数时父进程休眠一段时间


4.现在编写一个程序,在父进程中使用 wait(),等待子进程完成。wait()返回什么?如果你在子进程中使用 wait()会发生什么?  
 答:wait()返回子进程的 pid,子进程中调用无影响,返回值为-1。



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
close(STDOUT_FILENO);
int fd = open(“./p4.output”, O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
int rc = fork();
if (rc < 0)
{ // fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
}
else if (rc == 0)
{ // child: redirect standard output to a file
// now exec “wc”…
int wc=wait(NULL);
printf(“child,pid:%d,wc:%d\n”,getpid(),wc);
}
else
{ // parent goes down this path (main)
int wc = wait(NULL);
// sleep(1);
printf(“father,pid:%d,wc:%d\n”,getpid(),wc);
}
// if(fd>=0)
// {
// close(fd);
// }
return 0;
}


5.对前一个程序稍作修改,这次使用 waitpid()而不是 wait()。什么时候 waitpid()会有用?




| waitpid()参数值 | 说明 |
| --- | --- |
| pid<-1 | 等待进程组号为 pid 绝对值的任何子进程。 |
| pid=-1 | 等待任何子进程,此时的 waitpid()函数就退化成了普通的 wait()函数。 |
| pid=0 | 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用 waitpid()函数的进程在同一个进程组的进程。 |
| pid>0 | 等待进程号为 pid 的子进程。 |


使用getpgrp()获取当前进程组号  
 答:当 pid 为0(pid=0),-1(pid=-1),child\_pid(pid>0),getpgrp()\*-1(pid<-1)时,waitpid()有用


6.编写一个创建子进程的程序,然后在子进程中关闭标准输出(STDOUT\_FILENO)。如果子进程在关闭描述符后调用 printf()打印输出,会发生什么?  
 答:子进程无法打印,父进程无影响



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
// close(STDOUT_FILENO);
// int fd = open(“./p4.output”, O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
int rc = fork();
if (rc < 0)
{ // fork failed; exit
fprintf(stderr, “fork failed\n”);
exit(1);
}
else if (rc == 0)
{ // child: redirect standard output to a file
// now exec “wc”…
// int wc=wait(NULL);
close(STDOUT_FILENO);
printf(“child,pid:%d,wc:%d\n”,getpid());
}
else
{ // parent goes down this path (main)
// int wc = waitpid(getpgrp(),NULL,0);
// sleep(1);

    printf("father,pid:%d,wc:%d\n",getpid());
}
// if(fd>=0)
// {
// close(fd);
// }
return 0;

}

输出
root@hjk:~/repo/os_test# ./a.out
father,pid:11189,wc:0


7.编写一个程序,创建两个子进程,并使用 pipe()系统调用,将一个子进程的标准输出连接到另一个子进程的标准输入。  
 答:该程序将子进程2中的输出通过管道连接到子进程1的输入中



#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
int fds[2];
if(pipe(fds)==-1)
{
fprintf(stderr, “open pipe failed\n”);
exit(1);
}
int rc = fork();

 if (rc < 0)
 { // fork failed; exit
     fprintf(stderr, "fork failed\n");
     exit(1);
 }
 else if (rc == 0)
 { // child: redirect standard output to a file
     // int wc=wait(NULL);
     printf("child1,pid:%d\n",getpid());
     int len;
     char buf[10];
     // 从pipe中读取
     if((len=read(fds[0],buf,6))==-1)
     {
         perror("read from pipe");
         exit(1);
     }
     printf("buf:%s\n",buf);
     exit(0);
 }
 else { // parent goes down this path (main)
     // wait(NULL);
     //创建第二个子进程
     int rc2 = fork();
     if (rc2 < 0)
     { // fork failed; exit
         fprintf(stderr, "fork failed\n");
         exit(1);
     }
     else if (rc2 == 0)
     { // child: redirect standard output to a file
         // int wc=wait(NULL);
         printf("child2,pid:%d\n",getpid());
         char buf[]= "12345";
         // 写入pipe
         if(write(fds[1],buf,sizeof(buf))!=sizeof(buf))
         {
             perror("write to pipe");
             exit(1);
         }
         exit(0);
     }
 }

 return 0;

}


## (四) 机制:受限直接执行


* 操作系统需要以某种方式让许多任务共享物理 `CPU`
* 运行一个进程一段时间,然后运行另一个进程,如此轮换。通过以这种方式时分共享(`time sharing)CPU`,就实现了虚拟化 。 然而存在问题是性能(不增加额外开销)与控制权(权限)。


### 基本技巧:受限直接执行


* 为了使程序尽可能快地运行的技术 称之为受限的 直接执行`(limited direct execution) LDE` , 只需直接在`CPU` 上运行程序即可
* 使用正常的调用并返回跳转到程序的 `main()`,并在稍后回到内核。  
 ![](https://img-blog.csdnimg.cn/4f5fa1abaca64b4f8d3d6b825acb0d07.png#id=b23HD&originHeight=530&originWidth=1374&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
* 实际并没有怎么简单 ,如果对运行程序没有限制,`操作系统`将无法控制任何事情,因此会成为“仅仅是一个库” 。


### 问题 1:受限制的操作(特权问题 )


* 硬件与操作系统存在的问题 : 关键问题:如何执行受限制的操作??



> 
> 提示:采用受保护的控制权转移  
>  硬件通过提供不同的执行模式来协助操作系统。在`用户模式(user mode)`下,应用程序不能`完全访问`硬件资源。在`内核模式(kernel mode)`下,操作系统可以访问机器的`全部资源`。还提供了`陷入`(trap)内核和从`陷阱返回`(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。
> 
> 
> 


我们采用的方法是引入新的处理器模式:


#### 用户模式(user mode)


在用户模式下运行的代码会`受到限制`。例如,在用户模式下运行时,进程不能发出 I/O 请求。这样做会导致处理器引发异常,操作系统可能会终止进程。


#### 内核模式(kernel mode)


操作系统(或内核)就以这种模式运行。在此模式下,运行的代码可以做它喜欢的事,包括`特权操作`,如发出 I/O 请求和执行所有类型的受限指令。


#### 系统调用 (system call)


`系统调用`允许内核小心地向用户程序暴露某些`关键功能`,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。。  
 如果用户希望执行某种`特权操作`(如从磁盘读取),可以借助硬件提供的`系统调用`功能。  
 要执行系统调用,程序必须执行特殊的`陷阱`(trap)指令。该指令同时跳入内核并将特权级别提升到`内核模式`。一旦进入内核,系统就可以执行任何需要的`特权操作`(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的`从陷阱返回`(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。  
 执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够`正确返回`。



> 
> 陷阱如何知道在 `OS` 内运行哪些代码? 内核通过在启动时设置陷阱表(trap table)来实现.
> 
> 
> 


#### 陷阱表(trap table)


内核通过在启动时设置`陷阱表`(trap table)来实现陷阱地址的初始化。  
 当机器启动时,系统在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?  


![img](https://img-blog.csdnimg.cn/img_convert/7ff489f7ff75e38f72542d70213263f9.png)
![img](https://img-blog.csdnimg.cn/img_convert/f983eb26b2b7f923278bdd7b28f9cb91.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

小心地向用户程序暴露某些`关键功能`,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。。  
 如果用户希望执行某种`特权操作`(如从磁盘读取),可以借助硬件提供的`系统调用`功能。  
 要执行系统调用,程序必须执行特殊的`陷阱`(trap)指令。该指令同时跳入内核并将特权级别提升到`内核模式`。一旦进入内核,系统就可以执行任何需要的`特权操作`(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的`从陷阱返回`(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。  
 执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够`正确返回`。



> 
> 陷阱如何知道在 `OS` 内运行哪些代码? 内核通过在启动时设置陷阱表(trap table)来实现.
> 
> 
> 


#### 陷阱表(trap table)


内核通过在启动时设置`陷阱表`(trap table)来实现陷阱地址的初始化。  
 当机器启动时,系统在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?  


[外链图片转存中...(img-FEtFv7FY-1715847231235)]
[外链图片转存中...(img-OvHrdrAR-1715847231236)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值