目录
引言
- 操作系统的任务:在多个程序之间共享一台计算机,并提供比硬件本身支持的更有用的服务
- 一台计算机有许多进程,但只有一个内核
- 每个正在运行的程序,称为进程,都有包含指令、数据和堆栈的内存
- 当一个进程需要调用一个内核服务时,它会调用一个系统调用,系统调用进入内核;内核执行服务并返回
- Shell是一个普通的程序,它从用户那里读取命令并执行它们,不是内核的一部分
进程和内存
-
内核利用进程id或PID标识每个进程
-
pid_t pid = fork(); if (pid == -1) { // 错误处理 } else if (pid == 0) { // 我们现在处于子进程中 // ... 子进程的代码 ... } else { // 我们现在处于父进程中,pid 是子进程的 PID // ... 父进程的代码 ... }
-
父进程fork的返回值是子进程的pid,而子进程中的返回值是0 —>这是区别父子进程的方法
- 父子进程的内存内容完全一样
- 但运行中拥有不同的内存空间和寄存器 —> 所以一个进程改变的变量不会影响到另一个进程
-
fork
分配父内存的子副本所需的内存,exec分配足够的内存来保存可执行文件常见的使用模式是先调用
fork()
创建一个子进程,然后在子进程中调用exec
执行另一个程序。这允许原始程序继续运行,同时另一个程序在子进程中运行(也就是说,在子进程里成功执行exec后,子进程运行指定的程序【这里是
ls -l
命令】,而不会继续执行exec以下的代码)#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid; pid = fork(); // 创建一个子进程 if (pid < 0) { // fork失败 perror("Fork failed"); return 1; } if (pid == 0) { // 子进程 char *argv[] = { "ls", "-l", NULL}; // 参数列表,最后一个参数必须是NULL execvp(argv[0], argv); // 使用execvp因为它允许指定参数数组,并在PATH中查找命令,不需要写出ls的完整路径“/usr/bin/ls” perror("Exec failed"); // 如果execvp失败,这行将被执行 return 1; } else { // 父进程 int status; wait(&status); // 等待子进程结束 printf("Child finished with status %d\n", status); } return 0; }
I/O和文件描述符
把文件描述符理解为一个指针,指向一个文件
-
文件描述符:表示进程可以读取或写入的由内核管理的对象
- 进程可以通过打开一个文件、目录、设备,或创建一个管道,或复制一个已存在的描述符来获得一个文件描述符
- 将文件描述符所指的对象称为“文件”
-
read
和write
系统调用以字节为单位读取或写入已打开的以文件描述符命名的文件- 引用文件的每个文件描述符都有一个与之关联的偏移量
read
从当前文件偏移量开始读取数据,读取结束后,更新偏移量(write也一样)
-
fork
复制了文件描述符表,但是每个基础文件偏移量在父文件和子文件之间是共享(管道的读和取描述符也共享)
-
dup
是一个系统调用,调用dup
时,它会返回一个新的文件描述符,这个新的描述符指向原始描述符所指向的同一个文件、套接字或其他资源。—> 同理,这些描述符指向同一个文件,且共享用一个偏移量-
这常常用于文件描述符的重定向
-
例:重定向一个进程的标准输出到一个文件
-
解:使用
open
打开一个文件,得到一个文件描述符-
这会打开(或创建)一个名为 “output.txt” 的文件,并返回一个文件描述符
fd
。 -
int fd = open("output.txt", O_WRONLY | O_CREAT, 0666);
-
-
使用
dup2
或dup
来复制这个文件描述符到描述符 1(标准输出)-
意味着之后进程中的任何标准输出(例如,通过
printf
)都会写入 “output.txt” 而不是终端。 -
dup2(fd, 1); //1是指定生成的描述符,此时描述符fd和1都指向刚刚打开的那个文件
-
-
关闭原始的文件描述符。
-
已经完成了重定向,原始的文件描述符
fd
不再需要,可以关闭它 -
close(fd);
-
-
-
文件描述符0和1
-
0:标准输入
-
1:标准输出
-
Unix系统会从文件描述符为0所指的文件里读取数据,向描述符为1所指的文件里写数据
- 像wc命令,就默认从标准输入里读数据,并统计其文本数量。下面管道中的例子,就是将描述符0重定向到管道的读取端,所以当wc被执行时,它会从管道的读取端里读取数据。
ls existing-file non-existing-file > tmp1 2>&1
是个啥
-
ls existing-file non-existing-file
:ls
命令用于列出文件和目录。existing-file
是一个确实存在的文件(或至少命令假设它存在)。non-existing-file
是一个不存在的文件。
如果
existing-file
确实存在,ls
会正常显示它。但对于non-existing-file
,因为它不存在,ls
会输出一个错误消息到标准错误输出(通常是终端)。 -
> tmp1
: 这部分命令将标准输出(即ls
对existing-file
的正常输出)重定向到一个名为tmp1
的文件中。如果tmp1
不存在,它会被创建;如果已存在,它的内容会被覆盖。 -
2>&1
: 这部分命令涉及错误输出的重定向。2
代表标准错误输出。&1
指向标准输出的当前位置(在这个命令中,是tmp1
文件)。
这个重定向的意思是将标准错误输出重定向到与标准输出相同的位置。因此,
ls
对non-existing-file
的错误输出也会被重定向到tmp1
文件。
综上所述,这个命令会将
ls
的正常输出和错误输出都重定向到tmp1
文件中。如果existing-file
存在而non-existing-file
不存在,tmp1
文件中会包含existing-file
的名字和一个表示non-existing-file
不存在的错误消息。
管道
-
int fd[2]; int result = pipe(fd); //成功返回0
-
调用
pipe(fd)
时,操作系统会创建一个管道,并为这个管道的读取端和写入端分别分配两个文件描述符。这两个描述符会被存放在fd
数组中。- 描述符
fd[0]
会被初始化为指向管道的读取端。 - 描述符
fd[1]
会被初始化为指向管道的写入端。
也就是说,在调用pipe函数后,fd数组的两个成员就被初始化了,这两个描述符分别指向管道的读取端和管道的写入端
-
多个进程同时往管道里写数据时,如果数据量均小于PIPE_BUF字节,则认为写入操作是原子的,不会出现数据交错
-
若数据超过了PIPE_BUF,则但单个数据都会分多次写入,更别说多个进程了
-
管道的阻塞式读写
- 阻塞式读操作:
- 读取数据时,如果管道为空,那么读取操作会阻塞
- 当管道中有数据时,读进程会接收数据直到满足请求或管道被清空
- 阻塞式写操作:
- 写入管道时,如果管道已满(管道有大小限制),写操作将会阻塞
- 当管道有足够空间时,写操作继续
- 阻塞式读操作:
- 描述符
管道与进程通信
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if (fork() == 0) {
close(0); // 把0的文件描述符关了
dup(p[0]); // dup创建一个最小的文件描述符(也就是0,同时也是标准输入),使之指向管道的读取端
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);//使子进程开始执行【wc命令】,而wc命令需要给他统计的数据,默认是从标准输入中读取数据
} else {
close(p[0]);
write(p[1], "hello world\n", 12);.//向管道里写入hello world
close(p[1]);
}
解释:
当
fork
创建了一个子进程之后,父子进程会共享这个管道的文件描述符。但它们的使用方式不同:
- 子进程:
- 首先关闭它的标准输入(文件描述符0)。
- 然后通过
dup(p[0])
复制管道的读取端文件描述符到标准输入的位置。因为标准输入已经被关闭了,dup
会选择下一个可用的最小文件描述符,也就是0。- 接下来关闭它自己的
p[0]
和p[1]
,因为复制后已经不再需要它们。- 最后,执行
exec("/bin/wc", argv);
调用,这将执行wc
命令,它会从标准输入(现在已经指向管道的读取端)读取数据。- 父进程:
- 关闭管道的读取端,因为它不需要从管道读取任何数据。
- 向管道的写入端
p[1]
写入字符串"hello world\n"
。- 写入完成后,关闭管道的写入端,表示数据发送完成。
当子进程执行
wc
时,wc
试图从它的标准输入读取数据。因为标准输入已经被重定向到管道的读取端,所以wc
实际上是从父进程写入的管道读取数据。
文件系统
(好抽象)
-
文件和目录:
- 数据文件:包含字节序列,不解释内容。
- 目录:包含对数据文件和其他目录的命名引用,形成一个树状结构,从根目录
/
开始。
-
路径和工作目录:
- 绝对路径:从根目录开始,如
/a/b/c
。 - 相对路径:相对于进程的当前工作目录,可通过
chdir
系统调用改变。j
- 绝对路径:从根目录开始,如
-
文件操作:
- 创建文件:使用
open
系统调用和O_CREATE
标志。 - 创建目录:使用
mkdir
系统调用。 - 创建设备文件:使用
mknod
系统调用。
- 创建文件:使用
-
链接和删除:
-
link
系统调用:创建指向相同 inode 的另一个文件名。 -
unlink
系统调用:从文件系统中删除名称。当链接数为零且无文件描述符引用时,inode 和其内容被释放。
-
-
Inode:
-
Inode(索引节点):表示文件系统中的文件,包含文件类型、大小、磁盘位置等元数据。
-
链接:文件名与 inode 的关联,一个 inode 可以有多个链接(即多个名称)。
- 每个文件/目录都有一个“身份证号”(inode编号),包含这个文件的所有重要信息。
- 想象一个文件可以有多个名片(链接),每个名片上写着不同的名字,但是名片背后的人是同一个(即相同的inode编号)。
-
-
文件信息:
fstat
系统调用:从文件描述符引用的 inode 中检索信息,填充struct stat
结构体。
-
临时文件:
- 创建并立即
unlink
一个文件是创建没有名称的临时 inode 的惯用方法。
- 创建并立即
-
用户级命令:
- Unix 提供了如
mkdir
、ln
、rm
等可从 shell 调用的用户级文件操作程序。 cd
命令是内置在 shell 中的,因为它需要改变 shell 自身的当前工作目录。
- Unix 提供了如
2023.11.2
---------------------------------------------------------------
听课new理解
不同进程的表单、文件描述符、文件描述符空间
- 每个进程有独立的文件描述符空间和对应的表单
- 文件描述符空间里存储文件描述符
- 内核会为每一个运行进程保存一个表单,表单的key是文件描述符,表单的value是该文件描述符所指向的文件或资源
- 当一个进程访问一个文件描述符时,内核会查看这个表单,找到与这个文件描述符对应的value,然后根据这个value来操作相应的文件或资源
所以:
尽管两个不同的进程可能有相同的文件描述符数字(例如,都有一个描述符“3”),但由于他们的文件描述符空间和表单是独立的,这两个“3”可能指向两个完全不同的文件或资源
- 父子进程是两个不同的进程
- 在创建时会共享一些资源,如文件描述符表单
- 但在创建之后,由于分属于不同的进程,所以他们的文件描述符和表单不再同步和共享(独立的)
所以:
子进程关闭文件描述符3,并不会影响父进程的文件描述符3
如果父子进程都没有对描述3进行操作,则子进程往描述符3对应的文件里写东西,父进程读取描述符3对应的文件仍可能看到子进程写的东西
Shell与命令的运行
- 输入ls时,(实际的意义是我要求Shell运行名为ls的程序,文件系统中会有一个文件名为ls),是在要求Shell运行位于文件ls内的这些计算机指令。
- 在Shell中输入指令时【如ls】,Shell通过fork创建一个进程,并用exec来加载名为ls的文件中的指令。
exec系统调用
- exec系统调用会保留当前的文件描述符表单。
- 所以任何在exec系统调用之前的文件描述符,例如0,1,2等。它们在新的程序中表示相同的东西。
- exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了
- 所以,先fork拷贝整个父进程,再exec,将整个拷贝丢弃,并用将要运行的文件来替换拷贝好的内存,,,这属实有些浪费
父子进程
- 无法让子进程等待父进程
- 因为wait系统调用只能等待当前进程的子进程
- 且:
wait()
是有一个子进程结束就返回,它不会等待所有子进程都结束。每次wait()
调用只会处理一个子进程的结束 —> 如果要等待所有子进程结束,父进程需要多次调用wait
- 子进程拷贝父进程的所有内存,这里的内存指的是:
- 在编译之后,C程序看作是一些在内存中的指令,这些指令像数据一样存在于内存中,而字节数据可以被拷贝。
- 将父进程的内存镜像拷贝给子进程,并在子进程中执行。
2023.11.5