不同操作系统所暴露出的接口是不同的,因此Linux下的一些系统调用接口是无法移植到Windows下的。
文章目录
- 一、C语言中的文件接口
- 二、系统文件I/O
- 2.1.系统调用接口
- open
- 2.2.文件描述符fd(file descriptor)
- 2.3.补充内容--函数指针访问硬件
- 2.4.重定向的实现原理
- 三、FILE
- 四、dup 重定向
- 4.1.使用dup2 完成重定向
- 4.2.重定向恢复
- 4.3.在my_shell中添加重定向功能
- 总结
一、C语言中的文件接口
在C语言文件操作时学过文件接口C语言中的文件接口
这里补充一些内容:
文件=内容+属性
stdin:标准输入(键盘)
stdout:标准输出(显示器)
stderr:标准错误(显示器)
Linux下一切皆文件,所以stdin,stdout,stderr也是可以当做文件看待,被fopen打开。
这是因为语言是人和计算机交互的载体,所以任何一门语言都需要标准输入、输出和错误
之所以能直接使用printf和scanf这种输出到显示器和从键盘输入的函数,是因为C语言默认帮助我们打开了这三个标准输入和输出的设备。
用下面的代码验证:
可以看到普通文件和显示器文件在代码层面上是没有差别的。
二、系统文件I/O 2.1.系统调用接口
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,使用man手册可以查看他们:
fd:要打开文件的描述符
buf:写入/读入的字符串指针
count:写入/读入字符的个数(字节数)
实际上,我们上面用的C语言文件接口,在底层用的就是系统调用的接口:
系统调用的接口只有一套,和系统有关,而库函数可以有多个(C语言文件接口,C++文件接口等),语言层的库函数文件接口都是基于系统调用接口进行的封装。
由于显示器是硬件,硬盘也是硬件,所以任何语言的文件操作最终都要通过操作系统进行对硬件的写入。
open通过mani手册可以查看用法:
要包含的头文件: #include#include #include 打开的文件存在,使用下面的接口: int open(const char *pathname, int flags); 打开的文件不存在,使用下面的接口: int open(const char *pathname, int flags, mode_t mode); 参数: pathname:要打开文件的名字 flags:打开文件的方式 O_RDonLY :只读方式 O_WRonLY :只写方式 O_RDWR :读写方式 可选项: O_TRUNC :截断文件(清空文件内容) O_CREAT :文件不存在则创建文件 O_APPEND :追加的方式 O_EXCL | O_CREAT :如果文件存在,则打开文件失败 mode:创建文件时的权限(八进制,比如0664等) 返回值: 成功:新打开的文件描述符 失败:-1
可选项的原理:
所有选项对应的数在转成二进制后只有一个比特位为1且为1的二进制位是不同的,所以传多个选项的时候实际上是对传入的数进行按位或处理。最后传给flags的是一个数,这个数有一个或多个比特位为1。
以下面的代码为例:
2.2.文件描述符fd(file descriptor)可以看到打开成功后各自的返回值是3 4 5。。。
打开失败返回值则为-1
这些返回值叫做文件描述符,在系统层面是一个整数,从0开始。
其中:0为标准输入,1为标准输出,2为标准错误,这三个默认被打开
文件描述符的本质是数组下标,操作系统为每一个进程维护了一个文件描述符表,该表的索引值都从从0开始的,索引值都有一个指针指向对应的文件:
一个进程如何通过文件描述符找到文件:当进程执行write(4,"hello",5)时,进程先找到自己的PCB,PCB中包含文件描述符表(struct files_struct),然后通过索引下标4找到对应的文件。
所以如果将文件描述符为0(标准输入)的关掉,则分配的下标为0:
可以看出文件描述符的分配规则为:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
另外,要记得关闭文件描述符,否则会造成文件描述符泄漏,因为文件描述符表(数组)是有上限的。
2.3.补充内容–函数指针访问硬件不同的硬件访问方式是不一样的,对于外设访问的方式一般是读和写,但是实现代码是不一样的,此时进程就可以通过函数指针的方式来指向它们各自的实现方法:
重定向有两个操作符分别是>(格式化重定向)和>>(追加重定向),其实现原理就是操作系统内核把标准输出(1)关掉,然后打开对应的文件,此时该文件的下标就是1,应用层默认输入到下标为1的文件中,所以就输入到该文件中了。至于追加重定向则是在打开的时候加上选项O_APPEND。
三、FILE
文件指针中那个FILE本质是一个结构体,这个结构体中一定是包含文件描述符(fd)的,因为访问文件都是通过fd访问的。
对于下面的代码:
#include#include #include #include #include #include int main() { const char *msg0="hello printfn"; const char *msg1="hello fwriten"; const char *msg2="hello writen"; printf("%s", msg0); fwrite(msg1, strlen(msg0), 1, stdout); write(1, msg2, strlen(msg2)); fork(); return 0; }
如果对结果重定向,printf和 fwrite(库函数)都输出了2次,而write只输出了一次(系统调用)。
这是因为重定向影响了缓冲方式:
显示器采用的是行缓冲(遇到n就刷新)
文件采用的则是全缓冲(缓冲区数据写满才刷新)
没有重定向之前是往显示器写,而重定向则是往文件中写。
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf、fwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 全缓冲进程退出之后,会统一刷新,写入文件当中。
- fork的时候,父子数据会发生写时拷贝,数据被暂存在缓冲区中,因此子进程也就有了同样的一份数据,即产生两份数据。
- 由于父子进程的缓冲区中有两份一样的数据,所以会刷新两份。
- write 没有刷新两份,说明没有所谓的缓冲区。
我们这里所说的缓冲区,都是用户级缓冲区,printf和fwrite是库函数, write是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write没有缓冲区,而 printf和fwrite有,足以说明,该缓冲区是二次加上的,由C标准库提供。
另外fflush函数也在C标准库中,将用户级的缓冲区往系统中刷新:
下面是FILE的代码:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags
//缓冲区相关
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
char* _IO_buf_base;
char* _IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset;
#define __HAVE_COLUMN
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
四、dup 重定向
上面的重定向是将标准输出的fd关掉,然后打开重定向的文件。
而使用dup2函数就不需要关掉标准输出:
dup函数的作用是,返回一个新的文件描述符(可用文件描述符的最小值)newfd,并且新的文件描述符newfd指向oldfd所指向的文件表项。
比如:
文件描述符为1的文件通过dup重定向到文件描述符1上,那么也就相当于文件描述符3对应的文件也是显示器文件,那么向文件描述符3进行write,最终结果也会打印在显示器上。
4.1.使用dup2 完成重定向dup2有两个参数oldfd和newfd,newfd是oldfd的拷贝,将newfd重定向到oldfd,当整个函数调用成功后。会将newfd关掉,然后让newfd指向oldfd,总之,dup2函数的作用就是让newfd重定向到oldfd所指的文件表项上,如果出错就返回-1,否则返回的就是newfd。所以如果想让原本输出到显示器上的数据重定向到文件中,可以这样写:
在进行重定向后,如果想要恢复到重定向之前的状态,可以在重定向之前用dup函数保留该文件描述符对应的文件表项,然后在需要恢复重定向的时候使用dup2重定向到原来的文件表项,以重定向后恢复标准输出为例,如下所示:
4.3.在my_shell中添加重定向功能其实现原理是在子进程进行程序替换之前将标准输出(1)重定向到打开的文件中。
#include#include #include #include #include #include #include #include #define SIZE 256 #define NUM 16 //命令行参数的个数 void redirect(char* cmd) { int fd=-1; int redirect_count=0;//记录>的个数 char*file=NULL; char*ptr=cmd; while(*ptr) { if(*ptr=='>') { *ptr++=' '; redirect_count++; if(*ptr=='>') { *ptr++=' '; redirect_count++; } while(*ptr!=' '&&isspace(*ptr))//跳过空格 { ptr++; } file=ptr;//找到文件名 while(*ptr!=' '&&!isspace(*ptr))//清空文件名后面的空格 { ptr++; } *ptr=' '; if(redirect_count==1){ //> fd=open(file,O_CREAT|O_TRUNC|O_WRONLY,0644); } else if(redirect_count==2){ //>> fd=open(file,O_CREAT|O_APPEND|O_WRONLY,0644); } else{ //do nothing! } //文件已经打开,用dup2重定向 dup2(fd,1); close(fd); }//end if else if(*ptr=='<') { //和重定向>类似 } ptr++; } } int main() { char cmd[SIZE]; const char* cmd_line="[my_shell@VM-0-16-centos ~]# "; while(1) { cmd[0]=0; printf("%s",cmd_line); fgets(cmd,SIZE,stdin); cmd[strlen(cmd)-1]=' '; pid_t id=fork(); if(id<0) { perror("fork error!n"); continue; } if(id==0)//子进程 { redirect(cmd); char*args[NUM];//将命令字符串分割 args[0]=strtok(cmd," "); int i=1; do { args[i]=strtok(NULL," "); if(args[i]==NULL) { break; } i++; }while(1); execvp(args[0],args); exit(1); } int status=0; pid_t ret=waitpid(id,&status,0); if(ret>0) { printf("status code:%dn",(status>>8)&0xFF); } } return 0; }
总结
- 操作系统提供系统调用接口(open,close,read,write),C语言对系统调用接口进行封装
- FILE*是个结构体指针,FILE是个结构体,有两个比较重要的方面:文件描述符和缓冲区(C语言提供)
- fd是系统调用,fd是文件描述符表(数组)的下标,里面的内容是文件指针指向文件。系统默认打开0 1 2(标准输入,标准输出,标准错误)
- 重定向的底层是将fd的指向改变



