Xv6操作系统导论(第一章)
第一章 操作系统接口
操作系统的任务是在多个程序之间共享一台计算机,并提供比硬件本身支持的更有用的服务。操作系统管理和抽象底层硬件,例如文字处理器不需要关心使用哪种类型的磁盘硬件。一个操作系统在多个程序之间共享硬件,这样它们就可以(或者看起来可以)同时运行。最后,操作系统为程序提供了可控的交互方式,这样它们就可以共享数据或者一起工作。
操作系统通过接口向用户程序提供服务。设计良好的接口是很困难的。一方面,我们希望接口简单明了,因为这样更利于正确使用。另一方面,我们可能倾向于为应用程序提供许多复杂的特性。解决这个问题的诀窍在于设计接口时,依赖一些可结合的机制,以此来提供更好的通用性。
xv6这个操作系统提供了Ken Thompson和Dennis Ritchie的Unix 介绍的基本接口,并且模仿了Unix的内部设计。Unix提供了一个窄接口,其机制表现突出,提供了令人惊讶的通用程度。这个接口非常成功,甚至现代操作系统BSD、Linux、Mac OSX、Solaris,甚至在一定程度上,Microsoft windows都有类Unix的接口。理解xv6是理解这些系统和其他系统的一个良好开端。
如下图1.1所示,xv6采用传统的内核形式(内核是一个特殊的程序,为正在运行的程序提供服务)。每个正在运行的程序,称为进程,都有包含指令、数据和堆栈的内存。指令实现了程序的运算,数据是计算所依赖的变量,堆栈组织程序的过程调用。一台给定的计算机通常有许多进程,但只有一个内核。
当一个进程需要调用一个内核服务时,它会调用一个系统调用,这是操作系统接口中的一个调用。系统调用进入内核;内核执行服务并返回。因此,一个进程在用户空间和内核空间之间交替执行。
内核使用CPU提供的硬件保护机制来确保每个在用户空间执行的进程只能访问它自己的内存。内核程序的执行拥有操控硬件的权限,它需要实现这些保护;而用户程序执行时没有这些特权。当用户程序调用系统调用时,硬件会提升权限级别,并开始执行内核中预先安排好的函数。
内核提供的系统调用集合是用户程序看到的接口。Xv6内核提供了Unix内核传统上提供的服务和系统调用的子集。表1.2列出了xv6的所有系统调用。
系统调用 | 描述 |
---|---|
int fork()
|
创建一个进程,返回子进程的PID |
int exit(int status)
|
终止当前进程,并将状态报告给wait()函数。无返回 |
int wait(int *status)
|
等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。 |
int kill(int pid)
|
终止对应PID的进程,返回0,或返回-1表示错误 |
int getpid()
|
返回当前进程的PID |
int sleep(int n)
|
暂停n个时钟节拍 |
int exec(char *file, char *argv[])
|
加载一个文件并使用参数执行它; 只有在出错时才返回 |
char *sbrk(int n)
|
按n 字节增长进程的内存。返回新内存的开始 |
int open(char *file, int flags)
|
打开一个文件;flags表示read/write;返回一个fd(文件描述符) |
int write(int fd, char *buf, int n)
|
从buf 写n 个字节到文件描述符fd; 返回n |
int read(int fd, char *buf, int n)
|
将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0 |
int close(int fd)
|
释放打开的文件fd |
int dup(int fd)
|
返回一个新的文件描述符,指向与fd 相同的文件 |
int pipe(int p[])
|
创建一个管道,把read/write文件描述符放在p[0]和p[1]中 |
int chdir(char *dir)
|
改变当前的工作目录 |
int mkdir(char *dir)
|
创建一个新目录 |
int mknod(char *file, int, int)
|
创建一个设备文件 |
int fstat(int fd, struct stat *st)
|
将打开文件fd的信息放入*st |
int stat(char *file, struct stat *st)
|
将指定名称的文件信息放入*st |
int link(char *file1, char *file2)
|
为文件file1创建另一个名称(file2) |
int unlink(char *file)
|
删除一个文件 |
表1.2:xv6系统调用(除非另外声明,这些系统调用返回0表示无误,返回-1表示出错)
1.1 进程和内存
Xv6进程由用户空间内存(指令、数据和堆栈)和对内核私有的每个进程状态组成。Xv6分时进程: 它透明地在等待执行的进程集合中切换可用的CPU。当一个进程没有执行时,xv6保存它的CPU寄存器,并在下一次运行该进程时恢复它们。内核利用进程id或PID标识每个进程。
一个进程可以使用fork系统调用创建一个新的进程。Fork创建了一个新的进程,其内存内容与调用进程(称为父进程)完全相同,称其为子进程。Fork在父子进程中都返回值。在父进程中,fork返回子类的PID;在子进程中,fork返回零。例如,考虑下面用C语言编写的程序片段。
// fork()在父进程中返回子进程的PID
// 在子进程中返回0
int pid = fork();
if(pid > 0) {printf("parent: child=%d\n", pid);pid = wait((int *) 0);printf("child %d is done\n", pid);
} else if(pid == 0) {printf("child: exiting\n");exit(0);
} else {printf("fork error\n");
}
exit
系统调用导致调用进程停止执行并释放资源(如内存和打开的文件)。exit
接受一个整数状态参数,通常0表示成功,1表示失败。wait
系统调用返回当前进程的已退出(或已杀死)子进程的PID,并将子进程的退出状态复制到传递给wait
的地址;如果调用方的子进程都没有退出,那么wait等待一个子进程退出。如果调用者没有子级,wait
立即返回-1。如果父进程不关心子进程的退出状态,它可以传递一个0地址给wait
。
在这个例子中,输出
parent: child=1234
child: exiting
可能以任何一种顺序出来,这取决于父或子谁先到达printf
调用。子进程退出后,父进程的wait
返回,导致父进程打印
parent: child 1234 is done
尽管最初子进程与父进程有着相同的内存内容,但是二者在运行中拥有不同的内存空间和寄存器:在一个进程中改变变量不会影响到另一个进程。例如当wait
的返回值存入父进程的变量pid
中时,并不会影响子进程中的pid
,子进程中pid
仍然为0。
exec
系统调用使用从文件系统中存储的文件所加载的新内存映像替换调用进程的内存。(百度百科:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件)该文件必须有特殊的格式,它指定文件的哪部分存放指令,哪部分是数据,以及哪一条指令用于启动等等。xv6使用ELF格式(将会在第三章详细讨论)。当exec
执行成功,它不向调用进程返回数据,而是使加载自文件的指令在ELF header
中声明的程序入口处开始执行。exec
有两个参数:可执行文件的文件名和字符串参数数组。例如
char* argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
这个代码片段将调用程序替换为了参数列表为echo hello
的/bin/echo
程序运行,多数程序忽略参数数组中的第一个元素,它通常是程序名。
xv6的shell使用上述调用为用户运行程序。shell的主要结构很简单,请参见main
(user/sh.c:145)。主循环使用getcmd
函数从用户的输入中读取一行,然后调用fork
创建一个shell进程的副本。父进程调用wait
,子进程执行命令。例如:当用户向shell输入echo hello
时,runcmd
(user/sh.c:58) 将以echo hello
为参数被调用来执行实际命令。对于“echo hello
”,它将调用exec
(user/sh.c:78)。如果exec
成功,那么子进程将从echo
而不是runcmd
执行命令,在某刻echo
会调用exit
,这将导致父进程从main
(user/sh.c:78)中的wait
返回。
你或许想知道为什么exec
和fork
没有组合成为一个系统调用,稍后我们将会看到shell在其I/O重定向的实现中利用了这种分离。为了避免创建一个重复的进程然后立即替换它(使用exec
)的浪费,操作内核通过使用虚拟内存技术(如copy-on-write)优化 fork 在这个用例中的实现(见第 4.6 节)。
Xv6 隐式地分配大多数用户空间内存:fork
分配父内存的子副本所需的内存,exec
分配足够的内存来保存可执行文件。在运行时需要更多内存的进程(可能是malloc
)可以调用 sbrk(n)
将其数据内存增加n个字节; sbrk
返回新内存的位置。
1.2 I/O和文件描述符
文件描述符是一个小整数(small integer),表示进程可以读取或写入的由内核管理的对象。进程可以通过打开一个文件、目录、设备,或创建一个管道,或复制一个已存在的描述符来获得一个文件描述符。为了简单起见,我们通常将文件描述符所指的对象称为“文件”;文件描述符接口将文件、管道和设备之间的差异抽象出来,使它们看起来都像字节流。我们将输入和输出称为 I/O。
在内部,xv6内核使用文件描述符作为每个进程表的索引,这样每个进程都有一个从零开始的文件描述符的私有空间。按照惯例,进程从文件描述符0读取(标准输入),将输出写入文件描述符1(标准输出),并将错误消息写入文件描述符2(标准错误)。正如我们将看到的,shell利用这个约定来实现I/O重定向和管道。shell确保它始终有三个打开的文件描述符(user/sh.c:151),这是控制台的默认文件描述符。
read
和write
系统调用以字节为单位读取或写入已打开的以文件描述符命名的文件。read(fd,buf,n)
从文件描述符fd读取最多n字节,将它们复制到buf,并返回读取的字节数,引用文件的每个文件描述符都有一个与之关联的偏移量。read
从当前文件偏移量开始读取数据,然后将该偏移量前进所读取的字节数:(也就是说)后续读取将返回第一次读取返回的字节之后的字节。当没有更多的字节可读时,read
返回0来表示文件的结束。
系统调用write(fd,buf,n)
将buf中的n字节写入文件描述符,并返回写入的字节数。只有发生错误时才会写入小于n字节的数据。与读一样,write
在当前文件偏移量处写入数据,然后将该偏移量向前推进写入的字节数:每个write
从上一个偏移量停止的地方开始写入。
以下程序片段(构成程序cat
的本质)将数据从其标准输入复制到其标准输出。如果发生错误,它将消息写入标准错误:
char buf[512];
int n;
for (;;) {n = read(0, buf, sizeof buf);if (n == 0)break;if (n < 0) {fprintf(2, "read error\n");exit(1);}if (write(1, buf, n) != n) {fprintf(2, "write error\n");exit(1);}
}
代码片段中需要注意的重要一点是,cat
不知道它是从文件、控制台还是管道读取。同样也不知道它是打印到控制台、文件还是其他什么地方。文件描述符的使用以及文件描述符0是输入而文件描述符1是输出的约定允许了cat
的简单实现。
close
系统调用释放一个文件描述符,使其可以被未来使用的open
、pipe
或dup
系统调用重用(见下文)。新分配的文件描述符总是当前进程中编号最小的未使用描述符。
文件描述符和fork
相互作用,使I/O重定向更容易实现。fork
复制父进程的文件描述符表及其内存,以便子级以与父级在开始时拥有完全相同的打开文件。系统调用exec
替换了调用进程的内存,但保留其文件表。此行为允许shell通过fork
实现I/O重定向,在子进程中重新打开选定的文件描述符,然后调用exec
来运行新程序。下面是shell运行命令cat < input.txt
的代码的简化版本。
char* argv[2];
argv[0] = "cat";
argv[1] = 0;
if (fork() == 0) {close(0);open("input.txt", O_RDONLY);exec("cat", argv);
}
在子进程关闭文件描述符0之后,open
保证使用新打开的input.txt:0的文件描述符作为最小的可用文件描述符。cat
然后执行文件描述符0(标准输入),但引用的是input.txt。父进程的文件描述符不会被这个序列改变,因为它只修改子进程的描述符。
Xv6shell中的I/O重定向代码就是这样工作的(user/sh.c:82)。回想一下,在代码执行到这里时,shell已经fork
出了子shell,runcmd
将调用exec
来加载新程序。
open
的第二个参数由一组标志组成,这些标志以位表示,用于控制打开的操作。可能的值定义在文件控制(fcntl)头文件(kernel/fcntl.h:1-5)中
宏定义 | 功能说明 |
---|---|
O_RDONLY
|
只读 |
O_WRONLY
|
只写 |
O_RDWR
|
可读可写 |
O_CREATE
|
如果文件不存在则创建文件 |
O_TRUNC
|
将文件截断为零长度 |
现在应该很清楚为什么fork
和exec
分离的用处了:在这两个调用之间,shell有机会对子进程进行I/O重定向,而不会干扰主shell的I/O设置。我们可以想象一个假设的forkexec
系统调用组合,但是用这样的调用进行I/O重定向是很笨拙的。Shell可以在调用forkexec
之前修改自己的I/O设置(然后撤销这些修改);或者forkexec
可以将I/O重定向的指令作为参数;或者(最不吸引人的是)可以让每个程序(如cat)执行自己的I/O重定向。
尽管fork
复制了文件描述符表,但是每个基础文件偏移量在父文件和子文件之间是共享的,比如下面的程序:
if (fork() == 0) {write(1, "hello ", 6);exit(0);
} else {wait(0);write(1, "world\n", 6);
}
在这个片段的末尾,附加到文件描述符1的文件将包含数据hello world
。父进程中的写操作(由于等待,只有在子进程完成后才运行)在子进程停止写入的位置进行。这种行为有助于从shell命令序列产生顺序输出,比如(echo hello;echo world) >output.txt
。
dup
系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。这是另一种将“hello world”写入文件的方法:
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
如果两个文件描述符是通过一系列fork
和dup
调用从同一个原始文件描述符派生出来的,那么它们共享一个偏移量。否则,文件描述符不会共享偏移量,即使它们来自于对同一文件的打开调用。dup
允许shell执行这样的命令:ls existing-file non-existing-file > tmp1 2>&1
。2>&1
告诉shell给命令的文件描述符2是描述符1的副本。现有文件的名称和不存在文件的错误信息都会显示在tmp1文件中。Xv6 shell不支持错误文件描述符的I/O重定向,但是现在你知道如何实现它了。
文件描述符是一个强大的抽象,因为它们隐藏了它们所连接的细节:写入文件描述符1的进程可能写入文件、设备(如控制台)或管道。
1.3 管道
管道是作为一对文件描述符公开给进程的小型内核缓冲区,一个用于读取,一个用于写入。将数据写入管道的一端使得这些数据可以从管道的另一端读取。管道为进程提供了一种通信方式。
下面的示例代码使用连接到管道读端的标准输入来运行程序wc
。
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if (fork() == 0) {close(0);dup(p[0]);close(p[0]);close(p[1]);exec("/bin/wc", argv);
} else {close(p[0]);write(p[1], "hello world\n", 12);close(p[1]);
}
程序调用pipe
,创建一个新的管道,并在数组p中记录读写文件描述符。在fork
之后,父子进程都有指向管道的文件描述符。子进程调用close
和dup
使文件描述符0指向管道的读取端(前面说过优先分配最小的未使用的描述符),然后关闭p中所存的文件描述符,并调用exec
运行wc
。当wc
从它的标准输入读取时,就是从管道读取。父进程关闭管道的读取端,写入管道,然后关闭写入端。
如果没有可用的数据,则管道上的read
操作将会进入等待,直到有新数据写入或所有指向写入端的文件描述符都被关闭,在后一种情况下,read
将返回0,就像到达数据文件的末尾一样。事实上,read
在新数据不可能到达前会一直阻塞,这是子进程在执行上面的wc
之前关闭管道的写入端非常重要的一个原因:如果wc的文件描述符之一指向管道的写入端,wc将永远看不到文件的结束。
Xv6 shell以类似于上面代码(user/sh.c:100)的方式实现了诸如grep fork sh.c | wc -l
之类的管道。子进程创建一个管道将管道的左端和右端连接起来。然后对管道的左端调用fork
和runcmd
,对管道的右端调用fork
和runcmd
,并等待两者都完成。管道的右端可能是一个命令,该命令本身包含一个管道(例如,a | b | c
),该管道本身fork
为两个新的子进程(一个用于b,一个用于c)。因此,shell可以创建一个进程树。这个树的叶子是命令,内部节点是等待左右两个子进程完成的进程。
原则上,可以让内部节点在管道的左端运行,但是正确地这样做会使实现复杂化。考虑进行以下修改:将sh.c更改为不对p->left
进行fork
,并在内部进程中运行runcmd(p->left)
。然后,例如,echo hi | wc
将不会产生输出,因为当echo hi
在runcmd
中退出时,内部进程将退出,而不会调用fork
来运行管道的右端。这个不正确的行为可以通过不调用内部进程的runcmd
中的exit
来修复,但是这个修复使代码复杂化:现在runcmd
需要知道它是否是一个内部进程。同样的,当没有对(p->right)
执行fork
时也会更加复杂。例如,只需进行上述的修改,sleep 10 | echo hi
将立即打印“hi”,而不是在10秒后,因为echo
将立即运行并退出,而不是等待sleep
完成。因为sh.c的目标是尽可能的简单,所以它不会试图避免创建内部进程。
管道看起来并不比临时文件更强大:下面的管道命令行
echo hello world | wc
可以不通过管道实现,如下
echo hello world > /tmp/xyz; wc < /tmp/xyz
在这种情况下,管道相比临时文件至少有四个优势
首先,管道会自动清理自己;在文件重定向时,shell使用完
/tmp/xyz
后必须小心删除其次,管道可以任意传递长的数据流,而文件重定向需要磁盘上足够的空闲空间来存储所有的数据。
第三,管道允许并行执行管道阶段,而文件方法要求第一个程序在第二个程序启动之前完成。
第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。
1.4 文件系统
Xv6文件系统提供数据文件(包含未解释的字节数组)和目录(包含对数据文件和其他目录的命名引用)。这些目录形成一个树,从一个叫做根的特殊目录开始。像/a/b/c
这样的路径是指在根目录/
下名为a
的目录中名为b
的目录中名为c
的文件或目录。不以/
开始的路径相对于调用进程的当前工作目录进行计算,当前工作目录可以通过chdir
系统调用进行更改。下面两个代码片段打开相同的文件(假设所有相关的目录都存在)
chdir("/a");
chdir("b");
open("c", O_RDONLY);
open("/a/b/c", O_RDONLY);
上面代码将进程的当前目录更改为/a/b
;下面代码既不引用也不更改进程的当前目录
还有创建新文件和目录的系统调用:
mkdir
创建一个新目录open
中若使用O_CREATE
标志将会创建一个新的数据文件mknod
创建一个新的设备文件
这个例子说明了这三点:
mkdir("/dir");
fd = open("/dir/file", O_CREATE | O_WRONLY);
close(fd);
mknod("/console", 1, 1);
mknod
创建一个引用设备的特殊文件。与设备文件相关联的是主设备号和次设备号(mknod
的两个参数),它们唯一地标识了一个内核设备。当进程稍后打开设备文件时,内核将使用内核设备实现read
和write
系统调用,而不是使用文件系统。
一个文件的名字和文件本身是不同的;同一个底层文件(叫做inode,索引结点)可以有多个名字(叫做link,链接)。每个链接都由目录中的一个条目组成;该条目包含一个文件名和一个inode引用。Inode保存有关文件的元数据(用于解释或帮助理解信息的数据),包括其类型(文件/目录/设备)、长度、文件内容在磁盘上的位置以及指向文件的链接数。
fstat
系统调用从文件描述符所引用的inode中检索信息。它填充一个stat
类型的结构体,struct stat
在***stat.h(kernel/stat.h)***中定义为
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
struct stat {int dev; // 文件系统的磁盘设备uint ino; // Inode编号short type; // 文件类型short nlink; // 指向文件的链接数uint64 size; // 文件字节数
};
link
系统调用创建另一个文件名,该文件名指向与现有文件相同的inode。下面的代码片段创建了一个名字既为a又为b的新文件
open("a", O_CREATE | O_WRONLY);
link("a", "b");
从a读取或写入与从b读取或写入是相同的操作。每个inode由唯一的inode编号标识。在上面的代码序列之后,可以通过检查fstat
的结果来确定a和b引用相同的底层内容:两者都将返回相同的inode号(ino
),并且nlink
计数将被设置为2。
unlink
系统调用从文件系统中删除一个名称。只有当文件的链接数为零且没有文件描述符引用时,文件的inode和包含其内容的磁盘空间才会被释放,因此添加
unlink("a");
最后一行代码序列中会使inode和文件内容可以作为b访问。此外
fd = open("/tmp/xyz", O_CREATE | O_RDWR);
unlink("/tmp/xyz");
是创建没有名称的临时inode的惯用方法,该临时inode将在进程关闭fd或退出时被清理。
Unix以用户级程序的形式提供了可从shell调用的文件实用程序,例如mkdir
、ln
和rm
。这种设计允许任何人通过添加新的用户级程序来扩展命令行接口。事后看来,这个计划似乎是显而易见的,但是在Unix时代设计的其他系统经常将这样的命令构建到shell中(并将shell构建到内核中)
一个例外是cd
,它是内置在shell(user/sh.c:160)。cd
必须更改shell本身的当前工作目录。如果cd
作为常规命令运行,那么shell将分出一个子进程,子进程将运行cd
,cd
将更改子进程的工作目录。父目录(即shell的)的工作目录不会改变。
1.5 Real World
Unix将“标准”文件描述符、管道和方便的shell语法结合起来进行操作,这是编写通用可重用程序方面的一大进步。这个想法引发了一种“软件工具”的文化,这种文化对Unix的强大和流行做出了卓越贡献,shell是第一个所谓的“脚本语言”。Unix系统调用接口今天仍然存在于BSD、Linux和MacOSx等系统中。
Unix系统调用接口已经通过便携式操作系统接口(POSIX)标准进行了标准化。Xv6与POSIX不兼容:它缺少许多系统调用(包括lseek等基本系统调用),并且它提供的许多系统调用与标准不同。我们xv6的主要目标是简单明了,同时提供一个简单的类unix系统调用接口。为了运行基本的Unix程序,有些人扩展了xv6,增加了一些系统调用和一个简单的c库。然而,现代内核比xv6提供了更多的系统调用和更多种类的内核服务。例如,它们支持网络工作、窗口系统、用户级线程、许多设备的驱动程序等等。现代内核不断快速发展,提供了许多超越POSIX的特性。
Unix通过一组文件名和文件描述符接口统一访问多种类型的资源(文件、目录和设备)。这个想法可以扩展到更多种类的资源;一个很好的例子是Plan9,它将“资源是文件”的概念应用到网络、图形等等。然而,大多数unix衍生的操作系统并没有遵循这条路。
文件系统和文件描述符是强大的抽象。即便如此,还有其他的操作系统接口模型。Multics,Unix的前身,以一种看起来像内存的方式抽象了文件存储,产生了一种非常不同的接口风格。Multics设计的复杂性直接影响了Unix的设计者,他们试图使设计更简单。
Xv6没有提供一个用户概念或者保护一个用户不受另一个用户的伤害;用Unix的术语来说,所有的Xv6进程都作为root运行。
Xv6操作系统导论(第一章)相关推荐
- 网络空间安全导论-第一章习题
网络空间安全导论-第一章习题 1.网络空间安全有哪些定义? 2.简述网络安全空间的技术架构. 3.列举一些你身边遇到或发现的网络安全问题,试分析其中的原因,并说说有哪些防范措施. 1.网络空间安全有哪 ...
- 计算机导论第一章试题及答案,计算机导论第一章测试题
计算机导论第一章测试题 (4页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 19.90 积分 Test Bank-Chapter One (Data Re ...
- 操作系统习题——第一章
操作系统习题--第一章 1.设计现代OS的主要目标是什么? 答:(1)有效性 ( 2)方便性 ( 3)可扩充性 ( 4)开放性 2. OS 的作用可表现在哪几个方面? 答:(1) OS 作为用户与计算 ...
- 【王道考研】操作系统 笔记 第一章
特此鸣谢王道考研 本文参考王道考研的相关课程 若有侵权请联系,立删 其余笔记链接: [王道考研]操作系统笔记 第一章_才疏学浅743的博客-CSDN博客 [王道考研]操作系统 笔记 第二章上 进程调度 ...
- 概率论的基本公式(概率导论第一章)
概率论的基本公式(概率导论第一章) 文章目录 概率论的基本公式(概率导论第一章) 1. 概率模型 1.1 概率模型的基本组成 1.2 概率公理 1.3 概率律的若干性质 2. 条件概率 2.1条件概率 ...
- 【现代操作系统】第一章:引论
[现代操作系统]第一章:引论∑ 标签(空格分隔):[现代操作系统] 第一章:引论 操作系统:为用户程序提供一个更清晰的计算机模型,并管理输入/输出设备. 用户与之交互的程序,如果基于文本通常称为she ...
- Xv6操作系统导论(第二章)
第二章 操作系统架构 操作系统的一个关键要求是同时支持多个活动.例如,使用第1章中描述的系统调用接口,一个进程可以用fork启动新进程.操作系统必须在这些进程之间分时使用计算机资源.例如,即使进程比硬 ...
- 操作系统:第一章 计算机系统概述
本文已收录至 Github(MD-Notes),若博客中有图片打不开,可以来我的 Github 仓库:https://github.com/HanquanHq/MD-Notes,涵盖了互联网大厂面试必 ...
- 【操作系统】第一章 概论 冲鸭!!
第一章 概论 1.1 操作系统做什么? P1 操作系统是? 操作系统的目标 1.2 操作系统的功能 P2 计算机系统的组成 操作系统设计目的 用户视角 系统视角 操作系统的定义 1.3 计算机系统体系 ...
最新文章
- 从“元宇宙”热炒中理性看待我国虚拟现实产业发展
- “读书”频道的一些链接错误
- 关于日志系统显示SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“.
- nv4_disp.dll 蓝屏
- 编写properties文件的Eclipse插件
- ubunt 下 配置samba 服务器
- java日期时间各种变换及处理
- soidworks 生成PCD点云文件
- 【Flink】ERROR - Exception occurred in REST handler: Job xxx not found
- Python 表白?别傻了,女神是拿来撩的!
- 一纸书来只为墙,让他三尺又何妨?长城万里今犹在,不见当年秦始皇。
- linux 基础命令汇总
- 【2019百度之星初赛二1003=HDU6676】度度熊与运算式 1(思维)
- 好用的PDF解密工具哪个好?
- Application Repository一键启用微信告警通知
- Unity技术手册 - 粒子发射和生命周期内速度子模块
- 100集华为HCIE安全培训视频教材整理 | Agile Controller产品定位
- 计算机的变化作文,我的变化作文
- VUE3对比VUE2的优势及新特性原理
- numpy生成等差等比数列
热门文章
- 个人独立网站上线 http://www.16boke.com
- 设计师需要懂的数据指标与数据分析模型
- Adversarial Visual Robustness by Causal Intervention
- 如何把语音转文字,这个电脑语音转文字的方法就够了
- three.js简单实用
- 隐马尔科夫模型、三个基本问题、三个训练算法
- 山东省高校组队训练邀请赛——AC题目(4AC:3水题,1扩展欧几里得)
- 相空间重构维度m的选择:Cao法(matlab实现)
- springboot增加tp90监控
- 封装a.64p成.x64p达芬奇工具链的建立(工程编译步骤)g