西贝的代码宇宙

Back

Sleep#

Sleep 就是完成一个工具:sleep [time],输入后休眠 time 秒。

完成这个工具需要掌握两个关键点:

  1. 如何在 shell 中给程序传递参数
  2. Sleep 的原理

Main 函数传参#

C 语言中,Main 函数要么传递参数,要么传递参数。下面的示例写的很清楚。第一种不传递参数,直接在参数列表写一个 void ,不写也是可以的;第二种传递参数看起来就比较迷惑了。

int main(void) {
   // 无参数形式
   return 0;
}
int main(int argc, char *argv[]) {
   // 带参数形式
   return 0;
}
c

int argc, char *argv[],这到底传递的是什么?Argc 其实是参数的个数,agrv 则是参数的指针。也就是说我们 main 函数需要一个大小为 agrc 的列表,名字叫 argv,范围是 0-agrc-1 。

比如说我们执行 cp demo.c demo.cpp,agrc 为 3,argv 为 ['cp', 'demo.c', 'demo.cpp']。

那么 argc 和 argv 是如何得出的?这是 shell 的解析器在负责,不需要我们一个一个数参数到底有多少个。

总结一下:我们输入的指令,会被 shell 解析成 argc 和 argv ,然后交给 main 函数来使用。这里面还有更具体的流程,后面再说。

Sleep 的原理#

实验讲义里提示说让我们看看 sysproc.c 和 user.h。这就是实现 sleep 底层的代码。

user.h 提到了 sleep 系统调用的定义:int sleep(int);。传递一个整型,表示要休眠的时钟滴答数;返回一个整型返回值,返回 0 表示正常运行,返回 -1 表示非正常运行。这个 sleep 系统调用则是包装了下面的 sys_sleep。

sysproc.c 描述了 sys_sleep 实现:

这个系统调用有个好处:忙等检查和中间的让出机制,避免了程序持续对系统资源占用。想一想一个人在图书馆睡大觉,其他人还找不到图书馆的位置,这样是不是占用图书馆的资源呢?

解法#

现在知道了程序如何传参,以及系统调用 sleep 是如何实现的,现在我们就动手来写自己的 sleep 实现。

网上的解答非常多,都可以多多参照。我的解法重点在于异常的处理。

pingpong#

第二个工具是 pingpong。它的功能简单来说就是创建两个进程通过两个管道传递信息。先不关具体我们怎么做,首先我们应当搞清楚:什么是进程?什么是管道?怎么去使用?

进程#

进程就是正在运行的程序。 换句话说,你写的的代码是一个死气沉沉的文本文件,没有任何作用,只有让这个程序在计算机上运行起来,才能发挥作用。程序被计算机加载并运行,就成了进程。

操作系统调度进程。 电脑上时时刻刻都有数不清的进程在运行,但是 CPU 通常只有一个。这很像是小学经典奥数题:把水烧开需要 3 分钟,洗衣服需要 20 分钟,买蛋糕需要 5 分钟… 如何安排才能让自己完成这些事情的时间最少?计算机就面临这个问题,它要用有限的资源高效地做事情,就需要一个东西来调度进程执行的顺序与时间:什么时候运行这个进程,什么时候应该运行那个进程。这个活最开始是由人自己来做的,后来人太懒了,于是发明了操作系统,让操作系统帮忙调度进程的运行。

关于进程的创建,可以使用 fork系统调用 创建一个新的进程。fork 创建的新进程被称为子进程,其内存内容与调用的进程完全相同,原进程被称为父进程。在父进程和子进程中,fork都会返回一个值:在父进程中,fork返回子进程的PID(PID 可以理解为进程的编号,独一无二);在子进程中,fork返回0。

获取进程的 PID 可以通过 getpid,直接返回当前进程的 PID。

进程的退出则简单多了:exit 系统调用 会使得调用它的进程退出。exit 需要一个整数状态参数,通常0表示成功,1表示失败。

还有一个操作,这对父进程适用:wait 系统调用 返回当前进程的一个已退出的子进程的PID。如果调用者的子进程都没有退出,则等待这个子进程退出。如果调用者没有子进程,wait立即返回-1。

以上便是目前需要了解的进程知识。进程需要学习的东西还有很多,后面继续讲。

管道#

刚刚我们知道了进程是什么,而且还可以通过 fork 和 exit 来创建和退出进程。有意思的是进程不仅埋头苦干,还可以相互交流,他们之间交流的一种方式就是通过管道。想象一下,小时候你可能对着一个水管说话,水管的另一头还有个小伙伴用耳朵听你说的内容。这就是管道,进程就是通过如此“原始”的方法沟通的。

从上面的例子我们也可以看到,使用管道,必然有一个进程“说话”,另一个“倾听”,那我们该如何确定这个进程是在“说话”还是“倾听”呢?方法是文件描述符。文件描述符相当于一个整数“门票”,它标记了对文件的操作的入口。比如说我们想往一个文件写入字符串,那么我们只需要向这个文件的写入文件描述符发送字符串;如果我们想读取一个文件,只需要从这个文件的读取文件描述符读取字符串。

如果我们将管道看作文件,那么“说话”进程只需要向管道的写入文件描述符写入字符即可,“倾听”进程只需要向管道的读取文件描述符读取字符即可。(除了管道,UNIX 操作系统几乎把一切资源都视作文件,这是 UNIX 哲学的精髓)总的来说,使用管道,需要我们先创建管道的文件操作符,然后通过文件操作符来读取、写入信息。

创建一个管道需要用到 pipe 系统调用,例如:

int p[2];
if (pipe(p) < 0) {
	printf("pipe() failed\n");
	exit(1);
}
c

在通过 pipe 创建管道之前,我们定义了 p[2],这就是文件操作符。其中 p[0] 是读取,p[1] 是写入。pipe 需要传递这样的整数数组作为参数,同时返回一个整数值,创建失败则返回负数。

那么读取写入又是如何实现的呢?通过 read 和 write:

char buf = 'x';

// 参数列表:文件描述符、传递内容的地址、传递内容的大小(字节)
write(p[1], &buf, 1);
read(p[0], &buf, 1);
c

解法#

有了以上前提知识,我们就可以着手于 pingpong 程序的编写了。

MIT 6.S081 | Lab util
https://mrsibe.top/blog/mit-6s081/lab-util
Author 啊啊啊啊西贝
Published at 2025年9月28日