linux-基础IO

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
  • 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_*都是宏,它们都是一个整数。
    code

  • 上面就是这些O_*的源码。你想以什么方式打开文件就两个选项,是或者不是。这正好是二进制,上面的选项的特点是转换成二进制只有一个位置是1!那么按位或之后,我只需要查看flags的哪些位置是1,就具有哪些功能!你也可以知道,最多有32个功能,因为int只有32位。
    close

  • 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.
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*有什么关系呢?

std*
FILE
上面是我从stdio库中找到的源码。可以发现stdin其实质就是一个FILE*的指针而已!
_fileno

  • 而我在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遍。
    result
  • 而第二段代码,我们使用输出重定向重定向了1号文件描述符所对应的文件。

ret

我们可以看到结果:write(系统调用接口)打印了1次。而printf和fprintf(C语言库函数打印了两次).那么这背后有什么原因呢?

  • 两个结果对比,我们发现/n,即换行符号并不能使printf/fprintf刷新缓冲区,其实质是因为输出文件从显示器变成了文件(硬盘),所以刷新缓冲方式由行缓存变成了全缓冲!全缓冲意味着缓冲区满了或者使用fflush函数之后才会刷新!
  • 所以第二段程序在fork之后,缓冲区的数据是进程的一部分被fork出来,然后等待进程结束,刷新两次到文件中!
  • 而我们还发现write只打印了一次,这说明printf和fprintf的缓冲区不是系统提供的缓冲区,而是C语言提供的缓冲区,属于用户级别的缓冲区,所以才能作为数据被fork出来。
  • 而write就没有所谓的C语言层面的缓冲区。
  • 而write作为系统调用接口,printf和fprintf一定封装了write。由此我们甚至可以瞥见这两个函数内部的构造:先将数据写入用户缓冲区,然后调用write函数。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值