输入/输出(I/O) : 是指主存和外部设备(如磁盘,终端,网络)之间拷贝数据过程
在LINUX下,一切皆文件
-
一个Unix 文件就是一个m个字节的序列:
-
所有I/O设备都被模型化为文件。
-
而所有的输入和输出都被当做相应文件的读和写
-
设备优雅地映射成文件,允许Unix内核引出一个简单,低级的应用接口。叫做Unix I/O,使得所有的输入输出都能以一种统一且一致的方式来执行
打开和关闭文件
在c程序中,任何进程启动时,分别会启动三个输出流:stdin,stdout,stderro,
stdin: 标准输入,对应设备:键盘,
stdout:标准输出,对应设备显示器
stderro:标准错误,对应设备显示器
也就是说,如 print 或 scanf 函数都默认打开了这三个输出流,而在linux下也是一样。
打开文件
- 应用程序要求内核打开文件
- 内核返回一个小的非负整数,叫做文件描述符
- 等于内核分配一个文件名,来标示当前的文件。
- 内核记录有关这个打开文件的所有信息。应用程序只需要记住标示符。
文件描述符
什么是文件描述符?
文件描述符的本质其实是数组下标。如下图:
当我们打开一个目标文件时,操作系统要为目标文件创建相应的数据结构来描述这个文件,于是就有了File结构体,来表示已打开的文件。每一个进程都有一个File指针来指向File_struct结构体,这个结构体中包含一个指针数组,每一个元素都存放一个指向打开的文件的指针,下标为0,1,2的元素分别指向stdin,stdout,stderro标准输入,标准输出,标准错误三个文件,只要知道文件描述符就能知道对应的文件。
文件描述符分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
open函数
进程是通过open函数来打开或创建一个文件的
include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
//默认打开已经存在的文件
int open(const char *pathname, int flags, mode_t mode);
//打开不存在文件同时创建该文件,mode_t mode表示创造所需要的权限
open函数将filename转换为一个文件描述符,并且返回描述符数字。
返回的描述符总是在进程当前没有打开的最小描述符。
flags参数指明了进程打算如何访问这个文件:
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:返回-1;
例子代码
//已只读模式打开一个文件
fd = Open("foo.txt",O_RDONLY,0);
//打开一个已存在的文件,并在后面面添加一个数据
fd = Open("foo.txt",O_WRONLY|O_APPEND,0);
- 每个进程都有umask
- 权限掩码,或 权限屏蔽字
- 所有被设置的权限都要减去这个权限掩码才是实际权限。
- 777-022=755 或者是 777&~022。
- 通过umask()函数设置
#define DEF_MODE S_IRUSR|S_IWUSER|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
//所有人都能读和写
#define DEF_UMASK S_IWGRP|S_IWOTH //屏蔽了用户组的写和其他人的写
umask(DEF_UMASK);
fd=oepn("foo.txt",O_CREAT|O_TRUNC|O_WRONLY,DEF_MODE);
//创建了一个新文件,文件的拥有者有读写权利,其他人只有读权限。(屏蔽了用户组的写和其他人的写)
关闭文件
close函数关闭一个打开的文件
#include <unistd.h>
int close(int fd);
//返回: 若成功则为0,若出错则为-1
重定向
linux外壳提供了I/O重定向功能,允许用户将磁盘文件和标准输入输出联系起来。
例如:输出重定向,什么是输出重定向?
就是把原本应该打印到显示器的内容打印到相应的文件中,其中fd=1,常见的重定向有:>> ,>,<
代码如下:
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,00644);
if(fd<0)
{
printf("open erro");
return 1;
}
printf("fd:%d\n",fd);
fflush(stdout);
exit(0);
}
I/O重定向如何工作?
使用dup2函数
#include<unistd.h>
int dup2(int oldfd,int newfd);
//返回:若成功则为非负的描述符,若出错则为-1
dup2函数拷贝描述符表表项 oldfd 到描述符表表项 newfd ,覆盖newfd。
如果newfd已经打开,dup2会在拷贝oldfd之前关闭newfd.
原理:struct_File* fd array[ ] 是一个指针数组,array[]中存的都是地址,比如把输出显示器中内容输入到新创建的文件中,只要把array[ 3]中内容,也就是这个新文件的地址拷贝一份,赋给下标为1的元素,就可以了。
追加重定向
追加重定向就是在使用open函数时,采取追加的方式打开这个文件
int fd = open("log.txt",O_WRONLY|O_APPEND);
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
这一段代码的输出结果为:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file ,得到的结果却是这样的:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
这里write只打印了一次,而printf和write却打印了两次,我们知道这个结果肯定和fork()有关联
一般c库函数写入文件时是全缓冲的,而写入显示器是行缓冲的,printf和fwrite是自带缓冲区的,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲,而我们放入缓冲区的内容会不断被刷新,甚至fork()之后,但是进程退出之后会统一刷新,写入文件中,但是fork()之后父子进程会发生写时拷贝,所以当父进程刷新数据时,子进程会有一份同样的数据,随机产生两份数据,而write没有变化说明没有所谓的缓冲,
printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。