进程和内存
进程有自己的内存空间(指令 数据 和栈),翻译这段话真别扭。操作系统可以在一组需要执行的进程之间进行切换,当一个进程没有执行时,系统会保存进程的寄存器状态,在下一次执行的时候恢复状态。是不是很简单,简单,你写一个试试。内核会给每个进程分配一个进程标识,就是经常看到的pid。
利用fork可以创建一个新的进程,这个是什么意思?
看看fork
int 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");
}
首先父进程调用fork,产生子进程,子进程拥有和父亲相同内容的内存空间。最奇怪的就是fork调用会返回两次,所以后面就是两个程序了,就像影分身。在父进程里面返回子进程的进程id,在子进程里面返回0(为啥喜欢这样干,迷惑吧)。
exit调用会终止进程,且释放内存和文件资源。wait会等待子进程退出。
程序的输出
parent: child=1234// p
child: exiting //c
parent: child 1234 is done
p和c的顺序不是一定的,看谁首先获得调度,但是最后一句肯定在后面。现在两个程序有不同的寄存器和内存,改变一个变量不会影响另外一个。
exec这个系统调用,会从文件系统中加载一个可执行文件,替换掉当前的进程内存空间。这个文件需要符合一定的格式,包含指令,启动地址,和数据,调用以后直接执行启动命令。
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
这两个的区别是一个复制父进程,一个替换,是不是有点难以理解,为啥不新建一个(如果你也思考了这个问题,那后面就会有回答),再做处理,这可能是进程资源创建的问题。exec会分配足够的内存,如果不够用,再进行系统调用分配内存。
是不是感觉获得了一甲子功力?孩子别傻,现在还早,到现在一切还流于表面,后续的路还多着。请不要着急,耐心一点。
io和文件描述符
文件描述符是一个小整数,代表进程要读写的对象,这没有什么奇怪的,汇编里面也支持读写文件,在上一节中,可以在寄存器里填上值,代表不同的操作。
这里的文件描述符,代表一种抽象的文件,可以是文件,设备,管道,都当做一种字节流来处理。
每个进程都保存这一个文件资源表格,默认从0开始,代表输入,1代表输出,2代表错误。shell为了实现重定向(> <)和管道(|),要确保这几个总是打开的状态。
read会读取几个字节到buf里面,返回字节数,如果没有了就返回0。这里初学者比较难以理解的就是为啥要buf,一下把文件读入内存不就得了,得到我要的字符串就好了。这肯定没有考虑到大文件的情况,内存放不下的时候,一般测试者这里只是一个有几字节的小文件而已。刚开始嘛,想一下子解决问题,谁会想到用这些奇怪的技巧,但是起码得涨下经验吧。write类似就不说了。
基本的cat实现(单纯用c语言的getchar putchar 也可以实现)
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();
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit();
}
}
注意这里的cat基本不知道从哪里读,往哪里写,是文件还是管道。
还有一点没有提的就是,fork和exec调用都会保存父进程的文件表格,所以借用这些就可以实现重定向。
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);
}
因为在exec->cat后,进程的0文件还是input.txt里面的内容,当然这次1就成了控制台了。
其他
(真是个好的分类,什么都可以扔这里,因为到这里已经写的很累了,读者肯定也累了,也不要期望,一下子理解,因为我觉得不可能。我会分出一篇接口二。)
管道利用了一些类似的技巧,可以得到期望的文件描述符。
文件系统很值得学习的就是路径的表示方法,是树这种数据结构一种神来之笔,树真的是一种强大的数据结构,无处不在。互联网也有这种路径的表示方法的意思,包括最近rest接口定义。
文件实现相关的肯定需要了解磁盘的结构了,后面再说吧,cpu本身就有文件的读写指令。这种将各种设备资源抽象成文件的方法,受到人们的追捧,我也不知道好不好。
总之里面shell展示了系统调用(如wait close等)的灵活组装方式,真可以感叹一些这些精巧是如何想到的,起码对于我来说很不自然。
总结
一些目前学到的东西,以便更好的前行。首先是一些超凡脱俗的名词,看文章首的接口图片,还有一些没有讲到的自己查阅。大师是命名高手,一个词汇代表了太多的东西,绝逼是抽象派的。
良好的接口定义,可以让系统容易实现,且拥有灵活的组合方式,毕竟接口定义了这个操作系统,你说牛不牛。Java有jnr实现了一系列底层的接口,js都可以加载操作系统了。如今docker和openstack如日中天,你说不学点操作系统对得起谁?接口说完了,该说一下实现了,对不起这篇文章已经太长了,期待下一篇吧,下次要火力全开了。
这系列教程,是在阅读一些书籍和代码过程的记录,基本是xv6的翻译了(晕吧) 。如果你读到更好的文章推荐给我,我不写了。欢迎阅读我的其它文章。