minishell模拟实现
- shell的原理就是接收字符串,解析字符串,然后fork出子进程,用子进程程序替换完成命令,父进程则一直进程等待。然后使用大循环包住就行。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 256
#define NUM 10
int main(){
const char cmd_line[] = "[temp@zzh-linux base_IO]#";
char buf[SIZE] = {0}; //这个数组用来输入的字符串
char *args[NUM] = {0}; //这个数组用来接收解析后的数个字符串
while(1){
printf("%s",cmd_line);
fgets(buf, SIZE, stdin); // 从标准输入获取数据
buf[strlen(buf) - 1] = '\0'; // 去掉最后的空字符
char* p = strtok(buf, " "); //使用strtok解析字符串
int i = 0;
while(p){
args[i++] = p;
p = strtok(NULL, " ");
}
pid_t id = fork(); // fork子进程
if(id < 0){
perror("fork error");
exit(-1);
}
else if(id == 0){
//child
execvp(args[0], args); //进程替换子进程,去执行命令
exit(1);
}
else{
// parent
int status = 0;
pid_t ret = waitpid(id, &status, 0); // 进程等待
if(ret < 0){
printf("wait error");
exit(2);
}
if(ret == id){
printf("the return code : %d\n", (status >> 8) & 0xff); // 打印子进程退出码
}
}
}
return 0;
}
基础IO的复习
- 考虑这样一个问题,为什么我们输出数据使用的是键盘,而输出的数据和错误都显示到显示器上呢?因为操作系统默认帮助我们打开了3个文件,那就是stdin,stdout,stderr。它们代表标准输入,标准输出和标准错误。默认的输入输出都由这三者接收。
- C语言采用了缓冲区来帮助我们提高IO的效率,当数据要加载到内存中时,C程序会先将数据加载到缓冲区,等待缓冲区刷新,才被刷新到内存。同理,将数据从C程序输入到硬件中,也会先输入到缓冲区,然后才刷新到硬件设施中。具体的可以移步 。
系统调用的IO接口
- 往显示器上输出数据,往文件中写入数据,实际上都是将数据写入硬件。而用户无法直接访问硬件,必须穿过操作系统。所以所有的IO操作,都必须借助操作系统。操作系统为我们提供了一系列的接口帮助我们实现,
- open用于打开文件。其中pathname就是文件的路径,flags就是打开文件的方式。它的返回值是文件描述符,在linux中,文件描述符就是一个整数。open的第二个函数还有第三个参数,这个参数是被创建文件的权限,这个权限是跟umask有关的。
- open的第二个参数的设计非常厉害!在这个参数中你可以指定打开文件的方式,只读,只写,读写,如果你想要别的多个功能,例如追加那么你可以使用 | 来定制你的open。
int fd1 = open("a.txt", O_WRONLY | O_CREAT, 0644); //将文件以只写形式打开。如果文件不存在,则创建
int fd2 = open("b.txt", O_RDWR | O_APPEND, 0644); //将文件以可读可写形式打开。写入数据以追加形式。
-
你可能会对这种方式感到疑惑,为什么是按位或?为什么flags是一个整数?实质所以的O_*都是宏,它们都是一个整数。
-
上面就是这些O_*的源码。你想以什么方式打开文件就两个选项,是或者不是。这正好是二进制,上面的选项的特点是转换成二进制只有一个位置是1!那么按位或之后,我只需要查看flags的哪些位置是1,就具有哪些功能!你也可以知道,最多有32个功能,因为int只有32位。
-
close,关闭文件。fd就是文件描述符。
-
还有write,read等一系列系统调用接口。
对文件描述符的理解
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(){
int fd = open("a.txt", O_WRONLY | O_CREAT);
char buf[] = "hello world";
write(fd, buf, strlen(buf));
printf("%d", fd);
close(fd);
return 0;
}
- 上面的栗子是打开一个文件,如果不存在就创建它。写入hello world。这里要注意的是,写入的长度中不要将’\0’算进去!!因为’\0’只是C语言识别字符串结束的标准,而文件是不看这个的!
- 我们打印了文件的描述符,发现是3,但是这个文件是这个进程第一个被打开的文件,为什么不是0?没错,如果你注意到的话,系统会默认为我们打开3个文件,stdin,stdout,stderr。而这三个文件就是0,1,2.
- 再仔细想一想,0,1,2,3.。。。这像什么?这其实就是数组的下标。
- 如果创建一个空的文件,那么这个文件要不要占用空间?没错,空文件也要占用空间。因为文件有自己的属性信息!而文件包括文件内容和属性信息。那么打开很多文件,自然需要管理起来,怎么管理呢?先描述再组织!而系统中自然就会使用结构体描述文件,这个结构体叫做struct files{ },其中包含了文件的各种信息,包括位置,数据大小等信息。
- 那么文件是谁打开的呢?进程!而进程有pcb,那么我们需要建立pcb和files之间的关系!这样可以使进程访问到文件。
- 而linux建立关系的方式就是通过数组。在pcb中会有一个指针指向一个files_struct结构体,而这个结构体中会有一个叫做fd_array[]的结构体指针数组,这个数组的每个指针指向的就是打开的文件!此时我们就可以理解打开文件,其实质是加载文件到内存,建立文件的files,然后将pcb和files通过数组连接起来,需要使用的时候就找到该进程的pcb,然后找到指针指向的数组,找到文件描述符对应的数组指针,这样就拿到了文件!而是数组自然就有0,1,2,这些被系统默认占用了。
- 每个进程可以打开的文件数量是有限的,因为系统资源有限。系统为文件分配文件描述符的原则是分配最小的未被使用的。这个是因为files_struct中有一个next_fd,这个整形变量中存放着下一个被使用的fd。
Linux一切皆文件怎么理解?
- 我们知道C语言是一种面向过程的语言,但是C语言能不能用来实现面向对象呢?我们知道面向对象语言,例如C++,主要是两个点,成员变量和成员方法。而成员变量C语言是自己带有的。而对于成员方法我们可以这样:
struct file{
//属性信息
inode
//成员方法,函数指针
int (*write)(int fd, char* buf, int size);
(*open)
(*read)
}
而对于不同的硬件设施,它的读取方法,写入方法是不一样的,但是,我们可以封装一层。
- 对于硬盘,显示器,键盘,不同的硬件设施,有不同的read和write方法,但是我们在文件的结构体中,只提供了函数指针。
- 如果想要读取或是写入硬盘中数据,那么write就指向硬盘的h_write,h_read。如果想要读取或是写入显示器的数据,write和read就指向显示器对应的调用接口。
- 这样我们在驱动代码上层封装了一层虚拟层,而对于操作系统,它会认为我对于不同的硬件设施做的操作,都是相同的!!因为它们的file结构体相同!!而这就是面向对象的多态思想。通过这种思想,达到一切皆文件的目的!!
输出重定向的理解
-
我们知道1号的文件描述符是标准输入,如果我们将它关闭呢?那么我们再次打开一个文件,就会发现新打开的文件的文件描述符变成了1!!
-
我们将1号文件关闭,发现显示器上不会出现fd的文件描述符!我们使用cat查看a.txt文件,发现打出来是1!!没错,在上层看来,我们的1号文件就是标准输出文件,就是stdout,但是我们的底层早已经偷偷的换成了我们自己的a.txt文件,这样输出到显示器上的数据实际上就输出到了文件中,这就是重定向!而我们的追加重定向只需要将open的O_WRONLY选项改成O_APPEND就可以了。
-
这样我们就理解了bash中输出重定向的实现。检测到>符号,那么>符号后面的一定是文件名字。然后就关闭1号文件,此时开启我们要写入的文件,再利用输出函数输出前一个命令的处理结果即可。
dup2
我们知道了输出重定向的原理,但是自己实现起来雀氏有点麻烦,那么系统有没有什么便利的方法呢?系统为我们提供了一个接口,dup2.
- 实际上还有dup3,但是我只说dup2.这是最常使用的。dup2会怎么做呢?比如你已经打开了一个文件,它的描述符是5.dup2不会尝试去关闭1号文件。dup2会去将5号文件描述符数组对应的指针拷贝到1号中去,也就是说此时系统中1号和5号fd_array指针数组都指向你打开的文件,此时你就可以重定向!
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(){
int fd = open("a.txt", O_WRONLY | O_APPEND, 0644);
const char buf[] = "hello world\n";
dup2(fd, 1); // fd_array中 :将数组下标为fd的指针拷贝到数组下标为1的位置
close(fd); // 此时关闭fd,因为fd的指针已经拷贝到1了。
printf("%s", buf);//此时这个输出不会输出到显示器,输出到a.txt文件中!!!
return 0;
}
FILE && fd
一个栗子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(){
int fd = open("a.txt", O_WRONLY | O_APPEND, 0644);
const char buf[] = "write\n";
const char buf1[] = "printf\n";
const char buf2[] = "fprintf\n";
dup2(fd, 1); // fd_array中 :将数组下标为fd的指针拷贝到数组下标为1的位置
close(fd); // 此时关闭fd,因为fd的指针已经拷贝到1了。
write(1, buf, strlen(buf));
printf("%s", buf1);
fprintf(stdout, "%s", buf2);
return 0;
}
- 我们常说stdin,stdout,stderr,那么这些究竟是什么呢?通过上面的代码,我们发现当我们重定向到a.txt时,无论是write的写入文件描述符为1的文件,还是printf和fprintf的写入stdout,都会将数据写入a.txt,那么文件描述符和所谓的这些std*有什么关系呢?
上面是我从stdio库中找到的源码。可以发现stdin其实质就是一个FILE*的指针而已!
- 而我在libio库中找到了_IO_FILE的实现,其中有一个int的变量 _fileno,这个变量就是文件描述符!也就是说stdin,stdout,stderr其实质就是封装了文件描述符和其他文件信息的结构体的指针而已。 而这个结构体就是 _IO_FILE,我们常用的FILE只是它的typedef。
- 你可能会问有了文件描述符,为什么C库还要有自己的封装?这是一个基本问题,文件描述符是系统层面的,根据系统而变。而C库跟平台没有关系!大大增加了移植性。想一想,这也是多态!不管你的操作系统是什么,对外呈现的都是FILE。
- 第三张图我故意多截取了一大部分,这一部分就是我们常说的C语言提供的缓冲区。
- **这样我们就可以回答上面的问题,**为什么改变文件描述符1的指向,stdout不会变。因为它的里面封装了1号文件描述符,它也看不见dup2的操作!它也会认为1号文件描述符还是stdout!
一个栗子的改版
重定向之前:
int main(){
int fd = open("a.txt", O_WRONLY | O_APPEND, 0644);
const char buf[] = "write\n";
const char buf1[] = "printf\n";
const char buf2[] = "fprintf\n";
write(1, buf, strlen(buf));
printf("%s", buf1);
fprintf(stdout, "%s", buf2);
fork();
return 0;
重定向之后:
int main(){
int fd = open("a.txt", O_WRONLY | O_APPEND, 0644);
const char buf[] = "write\n";
const char buf1[] = "printf\n";
const char buf2[] = "fprintf\n";
dup2(fd, 1); // fd_array中 :将数组下标为fd的指针拷贝到数组下标为1的位置
close(fd); // 此时关闭fd,因为fd的指针已经拷贝到1了。
write(1, buf, strlen(buf));
printf("%s", buf1);
fprintf(stdout, "%s", buf2);
fork();
return 0;
- 我们做了一个小小的改动,在3句话都打印完成之后,我们fork一下。第一段代码是我们正常的输出到stdout上,没有重定向。结束是将3段话打印了1遍。
- 而第二段代码,我们使用输出重定向重定向了1号文件描述符所对应的文件。
我们可以看到结果:write(系统调用接口)打印了1次。而printf和fprintf(C语言库函数打印了两次).那么这背后有什么原因呢?
- 两个结果对比,我们发现/n,即换行符号并不能使printf/fprintf刷新缓冲区,其实质是因为输出文件从显示器变成了文件(硬盘),所以刷新缓冲方式由行缓存变成了全缓冲!全缓冲意味着缓冲区满了或者使用fflush函数之后才会刷新!
- 所以第二段程序在fork之后,缓冲区的数据是进程的一部分被fork出来,然后等待进程结束,刷新两次到文件中!
- 而我们还发现write只打印了一次,这说明printf和fprintf的缓冲区不是系统提供的缓冲区,而是C语言提供的缓冲区,属于用户级别的缓冲区,所以才能作为数据被fork出来。
- 而write就没有所谓的C语言层面的缓冲区。
- 而write作为系统调用接口,printf和fprintf一定封装了write。由此我们甚至可以瞥见这两个函数内部的构造:先将数据写入用户缓冲区,然后调用write函数。