OS_learning

chapter1:操作系统接口

操作系统的工作是:

  • 计算机资源在多个程序之间共享,并给程序提供一系列比硬件本身更有用的服务
  • 管理并抽象底层硬件。(如word不用关心自己使用的是哪种硬盘)
  • 多路复用硬件,让多个程序“看起来”是同时运行的。
  • 给程序间提供一种受控的交互方式,使得程序之间可以共享数据、共同工作

操作系统通过接口向用户程序提供服务。接口设计依赖于少量的机制,通过这些机制的组合提供强大、通用的功能。

  • kernel(内核):为运行的程序提供服务的一种特殊程序。每个运行着的程序叫做进程,每个进程的内存中存储指令、数据和堆栈。一个计算机可以拥有多个进程,但是只能有一个内核。
  • 每当进程需要调用内核时,它会触发一个system call(系统调用),system call进入内核执行相应的服务然后返回。
  • shell 是一个普通的程序,它接受用户输入的命令并且执行它们。不是内核的一部分。
  • xv6内核提供了Unix传统系统调用的一部分。
系统调用 描述
fork() 创建进程
exit() 结束当前进程;0代表以正常状态退出,1代表以非正常状态退出
wait() 等待子进程结束;等待子进程退出,返回子进程PID;没有子程序则返回-1
kill(pid) 结束 pid 所指进程
getpid() 获得当前进程 pid
sleep(n) 睡眠 n 秒
exec(filename, *argv) 加载并执行一个文件
sbrk(n) 为进程内存空间增加 n 字节
open(filename, flags) 打开文件,flags 指定读/写模式
read(fd, buf, n) 从文件中读 n 个字节到 buf
write(fd, buf, n) 从 buf 中写 n 个字节到文件
close(fd) 关闭打开的 fd
dup(fd) 复制 fd
pipe( p) 创建管道, 并把读和写的 fd 返回到p
chdir(dirname) 改变当前目录
mkdir(dirname) 创建新的目录
mknod(name, major, minor) 创建设备文件
fstat(fd) 返回文件信息
link(f1, f2) 给 f1 创建一个新名字(f2)
unlink(filename) 删除文件

进程和内存

  1. xv6进程由两部分组成:用户内存空间(指令,数据,堆栈)+ 仅对内核可见的进程状态
  2. 分时特性:在可用CPU之间不断切换。当一个进程不在执行时,xv6保存它的CPU寄存器,当他们再次被执行时恢复这些寄存器的值。
  3. 内核将每个进程和一个PID(process identifier)关联起来
  4. 一个进程可以通过调用fork()来创建新进程;创建生成的是子进程,其内容同创建它的进程(父进程)一样。
  • fork 函数在父进程、子进程中都返回(一次调用两次返回)。对于父进程它返回子进程的 pid,对于子进程它返回 0。
1
2
3
4
5
6
7
8
9
10
11
12
int pid;
pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait();
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit();
} else {
printf("fork error\n");
}

I/O和文件描述符

  • 文件描述符是一个整数,它代表了一个进程可以读写的被内核管理的对象
  • 获取方式:打开文件、目录、设备或创建一个管道pipe或复制已经存在的文件描述符。

管道

管道是一个小的内核缓冲区,它以文件描述符对的形式提供给进程,一个用于写操作,一个用于读操作。从管道的一端写的数据可以从管道的另一端读取。管道提供了一种进程间交互的方式。

  • 管道会进行自我清扫
  • 管道可以传输任意长度的数据
  • 管道允许同步

文件系统

xv6 文件系统提供文件和目录,文件就是一个简单的字节数组,而目录包含指向文件和其他目录的引用。

系统调用

read接受三个参数

  • 第一个参数:文件描述符,指向一个之前打开的文件。
    • shell会确保默认情况下,当一个程序启动时,文件描述符0连接到console的输入,文件描述符1连接到console的输出。
  • 第二个参数是指向某段内存的指针,程序可以通过指针对应的地址读取内存中的数据。
  • 第三个参数是代码想读取的最大长度。

Lab0 实验搭建

  • 使用wsl2。(在debian12中进行)
  1. 安装编译器(突然意识到,之前计组实验好像安装过了,所以直接使用)
  2. 安装QEMU
  3. 下载xv6并编译

遇到的问题:在 make qemu时出现了死循环错误。解决办法:修改makefile,将 CFLAGS := -Wall -O -fno-omit-frame-pointer -ggdb -DSOL_UTIL -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie 注释掉。

其余的直接安装官方步骤进行就可以了。

Lab1

sleep的实现

  1. 在user文件夹下新建sleep.c文件
  2. 研究一下其他函数文件是怎么实现的
  • 如何传递命令行参数
  • 头文件需要引用哪一些

例子:echo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
int i;

for(i = 1; i < argc; i++){
write(1, argv[i], strlen(argv[i]));
if(i + 1 < argc){
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}

直接使用同款头文件就行了

  • #include “user/user.h”这里涵盖了sleep()等系统调用函数
  • #include “kernel/types.h” 是一些数据类型
  • 获取参数:int main(int argc, char *argv[])
    • argc是参数个数
    • argv是输入的字符串
  • 将字符串转为int–atoi函数
  1. 写完sleep.c之后,要将它添加到makefile文件,一同编译出来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    UPROGS=\
    $U/_cat\
    $U/_echo\
    $U/_forktest\
    $U/_grep\
    $U/_init\
    $U/_kill\
    $U/_ln\
    $U/_ls\
    $U/_mkdir\
    $U/_rm\
    $U/_sh\
    $U/_stressfs\
    $U/_usertests\
    $U/_grind\
    $U/_wc\
    $U/_zombie\
    $U/_sleep\
  2. 然后再编译 make qemu,成功后,调用sleep函数就可以了

pingpong的实现

  1. 理解管道(pipe)
    • 管道是一种半双工的通信机制,允许两个进程进行通信。管道有两个文件描述符:
      • p[0]:用于读取数据
      • p[1]:用于写入数据
  2. 使用pipe()创建管道;此处需要创建两个管道,一个用于父级向子级发送一个字节;一个用于父级从子级读取字节
  3. read(fd, buf, n):从文件中读 n 个字节到 buf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc,char* argv[]){
int p1[2];
int p2[2];
pipe(p1);
pipe(p2);
int pid;
char buf[1];
pid = fork();
if (pid<0){
printf("ERROR:fork failed!");
exit(1);
}
else if (pid == 0)
{//子进程: 父进程从子进程读取数据--开启父进程的写,关闭读;开启子进程的读,关闭写
close(p1[1]);//关闭子进程的写
close(p2[0]);//关闭父进程的读
read(p1[0],buf, 1);//从子进程的读口读出1个字节到buf
printf("%d:received ping\n", getpid());
write(p2[1], " ", 1);//向父进程的写口,写入一个字节
close(p1[0]);//关闭子进程的读口
close(p2[1]);//关闭父进程的写口
exit(0);//正常退出
}
else
{//父进程:父进程写入子进程--开启父进程读口和子进程写口;关闭父进程写口和子进程读口
close(p1[0]);
close(p2[1]);
write(p1[1], " ", 1);
read(p2[0], buf, 1);
printf("%d:received pong\n",getpid());
close(p1[1]);
close(p2[0]);
exit(0);
}
}

primes

=>素数筛法
素数筛法是一种经典的算法,用于找出一定范围内的所有素数。它的基本思想是:

  • 从 2 开始,将当前数标记为素数。
  • 将该数的所有倍数标记为非素数。
  • 移动到下一个未被标记的数,重复上述过程。
    在并发版本中,每个素数会由一个独立的进程处理,并通过管道将筛选后的数字传递给下一个进程。

=>并发版本的思路
在并发版本中,每个素数对应一个进程,进程之间通过管道通信:

  1. 第一个进程:生成数字 2 到 35,并将它们写入管道。
  2. 后续进程:
  • 从管道中读取第一个数字,这个数字就是当前进程负责的素数。
  • 打印这个素数。
  • 继续读取后续的数字,筛选掉当前素数的倍数,将剩余的数字写入下一个管道。
  • 创建一个新的子进程来处理下一个素数。
  1. 终止条件:
  • 当没有更多数字需要处理时,进程退出。

=>需要两个管道,一个用于筛选得到新的一组数据,另一个用于接收新一组的数据,进一步进行筛选;
直到达到限制

=>文件描述符的管理:
每个进程需要关闭不需要的文件描述符,以避免资源耗尽。
父进程在创建子进程后,需要关闭子进程使用的管道端。

=>进程的递归创建:
每个素数对应一个进程,进程之间通过管道连接,形成一条链。

=>终止条件:
当管道中没有更多数字时,进程退出。
主进程需要等待所有子进程退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void sieve(int fd){
int num;
//read(fd, &num, sizeof(num));//读取第一个数,作为素数
if (read(fd, &num, sizeof(num)) == 0) {
close(fd);
return; // 读取失败,退出
}
printf("prime %d\n", num);//打印目标语句
//筛选掉当前素数的倍数
int p[2];
pipe(p);
int pid = fork();
if(pid==0){
//递归
close(p[1]);
sieve(p[0]);
close(p[0]);
exit(0);
}
else
{
//筛选
close(p[0]);
int n;
while(read(fd,&n,sizeof(n))>0){
if(n%num!=0){
//不是当前素数的倍数,保留
//传入到下一个
write(p[1], &n, sizeof(n));
}
}
close(fd);
close(p[1]);
wait(0);//等待子进程退出
}
}

int main(int argc,char* argv[]){
int p[2];
pipe(p);
int pid = fork();
if(pid==0){
//child process
close(p[1]);
sieve(p[0]);//从子进程读口获取新一组数据
close(p[0]);
exit(0);
}else{
//parent process
close(p[0]);
for (int i = 2; i <= 35;i++){
write(p[1], &i, sizeof(i));//2-35
}
close(p[1]);
while (wait(0) != -1);
}
exit(0);
}

find的实现

  1. 阅读ls.c文件,了解如何获取目录信息

    • dirent:读取目录项
      1
      2
      3
      4
      5
      struct dirent {
      ushort inum; // i-node 号,指向文件系统中的 i-node
      char name[DIRSIZ]; // 文件/目录名(不含路径)
      };

    • stat:获取文件属性
      1
      2
      3
      4
      5
      6
      7
      8
      struct stat {
      int dev; // 设备号
      uint ino; // i-node 号
      short type; // 文件类型 (T_DIR=目录, T_FILE=普通文件)
      short nlink; // 硬链接数
      uint size; // 文件大小
      };

    • dirent 结构本身不会告诉你文件是目录还是普通文件 ,要用 stat() 查询。
  2. 读取目录:

    • 用read()读取目录项
      1
      2
      3
      4
      5
      6
      int fd = open(".",O_RDONLY);
      struct dirent de;
      while(read(fd,&de,sizeof(de))==sizeof(de)){
      printf("File:%s\n",de.name);
      }
      close(fd);
    1. 获取文件属性
1
2
3
4
5
	struct stat st;
if (stat("filename", &st) == 0) {
printf("Size: %d, Type: %d\n", st.size, st.type);
}

xargs的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"

// 读取一行输入,存储到 buffer
int read_line(char *buffer, int max_size) {
char c;
int i = 0;

while (read(0, &c, 1) == 1) { // 从标准输入读取一个字符
if (c == '\n') { // 行结束
buffer[i] = 0; // 以 NULL 结尾
return i;
}
if (i < max_size - 1) { // 防止溢出
buffer[i++] = c;
}
}

return i > 0 ? i : -1; // 返回读取的字节数,如果是 EOF 则返回 -1
}

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: xargs <command> [args ...]\n");
exit(1);
}

char buffer[512]; // 存储输入行
char *exec_argv[MAXARG]; // exec() 参数数组
int i;

// 复制原始命令参数(跳过 xargs 本身)
for (i = 0; i < argc - 1 && i < MAXARG - 1; i++) {
exec_argv[i] = argv[i + 1];
}

while (read_line(buffer, sizeof(buffer)) != -1) {
if (i >= MAXARG - 1) {
printf("xargs: too many arguments\n");
exit(1);
}

exec_argv[i] = buffer; // 把读取到的行作为新参数
exec_argv[i + 1] = 0; // exec 需要 NULL 终止

if (fork() == 0) {
exec(exec_argv[0], exec_argv);
printf("xargs: exec failed\n");
exit(1);
} else {
wait(0); // 等待子进程完成
}
}

exit(0);
}


OS_learning
https://pqcu77.github.io/2025/02/08/OS-learning/
作者
linqt
发布于
2025年2月8日
许可协议