linux基础IO
Posted 可乐不解渴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux基础IO相关的知识,希望对你有一定的参考价值。
愿所有梦见远方的人,心有惊雷,生似静湖。
基础IO
前言
一、C语言中文件的IO操作
r :只读方式打开一个已经存在的文本文件,使文件指针指向文件的首地址。如果指定文件不存在会出错。
w:只写方式打开一个文本文件。如果指定文件不存在则会创建一个新的文件。
a:向文本文件末尾添加数据。如果指定文件不存在会出错。
fopen函数打开文件
函数原型:FILE *fopen(const char *path, const char *mode);
path:为所要打开的文件的路径(加上要打开的文件名)。这里可以给绝对路径也可以给相对路径。
mode:以何种方式打开,如上面讲的几种等等。
头文件: #include <stdio.h>
返回值:
1、成功返回文件流指针FILE*。
2、失败则返回NULL。
fread函数读文件
函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:要读到哪里,传入到ptr这个位置就是将从文件读到的内容写到ptr所指向的空间中。
size:定义读文件时,一次读多少字节的内容,单位为字节。
nmenb:期望可以从文件中读取到多少字节的内容。
stream:文件流指针,从哪里读。(一般是stdout,或者是自己打开的文件流指针)
头文件: #include <stdio.h>
**返回值:**返回的是实际读取到的字节个数。
fwrite函数写文件
函数原型:size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
ptr:为指向要写入到文件里的内容。
size:一次写多少字节的内容。
nmemb:总共要写如多少字节到文件中。
stream:文件流指针,写到哪里。(一般是stdout,或者是自己打开的文件流指针)
stdin&stdout&stderr
-
在C语言标准中,会默认给我们打开三个文件流指针,分别是stdin、stdout、stderr。
-
stdin:默认是从键盘这个文件中读入数据。
-
stdout与stderr:默认是将数据写入到显示屏这个文件中。
-
仔细观察发现,这三个流的类型都是FILE*, 也就是fopen返回值类型,文件流指针。
二、系统I/O操作
在上述的C语言文件操作接口中,其实本质就是C语言的库函数对系统I/O操作进行了封装得到的,这样做使得语言有了跨平台性,也方便进行二次开发。
下面我们就来讲解以下系统的I/O接口。
open函数
open函数有两个版本,相当于是C++的函数重载一般。函数原型如下:
版本一:int open(const char *pathname, int flags);
版本二:int open(const char *pathname, int flags, mode_t mode);
open函数的头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
由于版本二参数更多且操作前两个参数和版本一相同,这里我们以版本二为例。
open的第一个形参
pathname:表示要打开或创建的目标文件,可以给绝对地址也可以是相对地址,这里和C语言的接口fopen相同。
open的第二个形参
flags:表示的打开文件的方式。
常用的选项如下表所示:
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRONLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,则创建文件 |
其中这些选项可以组合起来使用,与C++的文件操作类似如:ios::out|ios::app
在系统调用接口里我们也可以这样组合O_WRONLY | O_CREAT
,这个组合的选项的含义为:以只写的方式打开文件,如果文件不存在,则自动创建一个新的文件。
为什么用的是 | 按位或这个组合呢?
这是因为系统调用接口的open函数的flag参数是一个整形,占4个字节有32个比特位,然后将一个比特位作为一个选项的标志位,这就像位图的一样了,利用一个比特位来对应一种选项,之后使用的时候只需要检测该比特位是否为1,如果为一说明该选项被使用了。
open的第三个形参
mode:表示的是如果创建了文件的权限,但该权限会受到umask的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002。这个选项最好输入八进制,因为umask的值也是八进制表示。
如果不想让我们创建的文件不受umask的影响有两种方法,但原理相同。
1、直接在linux上修改umask的值为0000。
2、在我们的写open函数前加一句代码umask(0)
即可。
umask这个函数功能是修改umask的值。
umask函数所在的文件为:
#include <sys/types.h>
#include <sys/stat.h>
当我们已经知晓文件已经存在,也就不需要填入第三个参数了,所以这就是为什么会有两个版本的open的原因。
open函数的返回值
1、成功:新打开的文件的文件描述符。
2、失败:返回-1。
既然我们知道了open函数的返回值是一个文件描述符,那么该文件描述符是怎么分配的呢?
那么我们在这里就多创建几个文件看看它的分配规则是怎样子的,以如下代码所示:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
int fd1=open("Clog1.txt",O_CREAT|O_WRONLY,0666);
int fd2=open("Clog2.txt",O_CREAT|O_WRONLY,0666);
int fd3=open("Clog3.txt",O_CREAT|O_WRONLY,0666);
int fd4=open("Clog4.txt",O_CREAT|O_WRONLY,0666);
int fd5=open("Clog5.txt",O_CREAT|O_WRONLY,0666);
int fd6=open("Clog6.txt",O_RDONLY); //这个文件不存在,fd6的值为-1
printf("fd1:%d\\n",fd1);
printf("fd2:%d\\n",fd2);
printf("fd3:%d\\n",fd3);
printf("fd4:%d\\n",fd4);
printf("fd5:%d\\n",fd5);
printf("fd6:%d\\n",fd6);
return 0;
}
结果下图所示:
close函数
头文件:#include<unistd.h>
函数功能:关闭该进程的一个文件描述符,即关闭该文件描述符所对应的文件。
函数原型: int close(int fd);
write函数
头文件:#include<unistd.h>
函数原型: ssize_t write(int fd, const void *buf, size_t count);
**函数功能:**往当前fd所对应的文件中写入数据。
fd:文件描述符。
buf:数据的来源,要写入的数据。
count:写入多少个字节。
返回值:
1、如果顺利写入,write()会返回实际写入的字节数(len)。
2、当有错误发生时则返回-1,错误代码存入errno中。
下面我们就写一段代码来演示以下write的用法:
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
int fd=open("log.txt",O_CREAT|O_WRONLY,0666);
if(fd<0) //检测是否打开失败
{
perror("open error\\n");
}
const char * buff="hello CSDN\\n"; //这里加个\\n是为了输出的换行
//将buff的内容写入到fd所对应的文件log.txt中
write(fd,buff,strlen(buff));
return 0;
}
结果如下:
read函数
头文件:#include<unistd.h>
函数原型: ssize_t read(int fd, void *buf, size_t count);
函数功能:
fd:文件描述符。
buf:读到哪个位置。
count:读多少个字节。
返回值:
1、返回实际读到了多少字节。
2、读到文件结束返回0。
3、读文件错误返回-1。
同理我们这里也写一段代码来演示以下read的用法。
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd=open("log.txt",O_CREAT|O_RDONLY);
if(fd<0) //检测是否打开失败
{
perror("open error\\n");
}
char buff[100];
ssize_t i=read(fd,buff,sizeof(buff));
buff[i]='\\0';//为什么这里要加这一句呢,因为我们刚刚写入的数据是没有斜杠零的,但我们下面要以%s的形式打印,必须得将其字符串尾部加入\\0
printf("%s",buff);
return 0;
}
结果如下:
三、文件及文件描述符
在上面的通过对open函数的学习,我们知道了文件描述符就是一个小整数。
文件是由进程运行时打开的,一个进程可以打开多个文件,而在系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
既然有怎么多文件,就需要OS去管理,那么怎么去管理呢?
就可以拿出我们的六字真言:先描述,在组织。
在系统内部每一个被打开的文件都被会被描述成strcut file的一个类型。
那么这些被进程打开的文件是怎么和进程联系起来的呢?
我们知道每一个可执行程序从磁盘被加载到内存就变成了进程,此时操作系统就会给该进程创建task_strcut也就是PCB、mm_struct(进程地址空间)、以及页表。此时该进程PCB当中会保存一个struct file_struct
类型的一个名叫file的指针,该指针指向struct file_struct
类型的一个对象,该对象内部保存了一个最重要的一个元素就是 strcut file*fd_array[32]
的一个指针数组中,该数组中存的是当前该进程打开的文件所对应指向打开文件的指针。并且每一个文件的对应的指针所存储到该数组中的下标即是该文件所对应的文件描述符。
所以我们就知道了文件描述符的本质就是:就是一个数组的下标,该数组内存储的是一个file *,用来指向打开的文件。
所以当我们知道了某个文件的文件描述符(下标),只需要去这个fd_arrary这个指针数组中去对应的下标中就可以找到该文件,进而对文件进行一系列 I/O 操作。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
文件描述符的分配规则
下面我们先一次性打开3个文件。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
int fd1=open("Clog1.txt",O_CREAT|O_WRONLY,0666);
int fd2=open("Clog2.txt",O_CREAT|O_WRONLY,0666);
int fd3=open("Clog3.txt",O_CREAT|O_WRONLY,0666);
printf("fd1:%d\\n",fd1);
printf("fd2:%d\\n",fd2);
printf("fd3:%d\\n",fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
在这里我们会发现默认是从3开始分配的。那么0、1、2去哪里了呢?
答:因为我们知道文件描述符本质就是fd_array数组的下标,而当进程被创建时就默认打开了标准输入流、标准输出流和标准错误流。0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器,所以这里从3开始分配咯。
那么我们将某个文件关闭,那么是从即开始分配呢?
我们将stdin与stderr关闭,那么我们此后文件描述符的分配又会是怎样的呢?
close(0);
close(2);
结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的
四、重定向
重定向的原理
在上面我们以及知道了文件描述符的分配规则,下面我们就能很好的知道啊重定向的原理。
重定向的本质就是修改文件描述符下标对应的struct file*的内容。
输出重定向
输出重定向是指将我们本应该输出到一个文件的数据重定向输出到另一个文件中。
如果我们想让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
close(1);
int fd=open("log.txt",O_CREAT|O_WRONLY,0666);
if(fd<0) //检测是否打开失败
{
perror("open error\\n");
}
printf("hello CSDN\\n");
printf("hello CSDN\\n");
printf("hello CSDN\\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果后,我们发现显示器上并没有输出数据,对应数据输出到了log.txt文件当中。
注意:
1、printf等C语言接口默认向stdout输出数据的,而stdout是一个struct FILE*的一个指针,指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。然后此时关闭了之前往显示屏里打,然后再打开一个文件此时这个刚创建的文件的文件描述符就是1,此时printf就往我们刚刚创建的文件中打印内容。
2、C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言提供的用户级缓冲区当中,但这里涉及到了重定向,尽管你带了**\\n**也不会行刷新,这是因为重定向后,这里的缓冲区行缓冲变成全缓冲,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
追加重定向
追加重定向其实与重定向没有什么关系,即就是再打开文件的时候多加一个O_APPEND选项。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
close(1);
int fd=open("log.txt",O_CREAT|O_WRONLY|O_APPEND);
if(fd<0) //检测是否打开失败
{
perror("open error\\n");
}
printf("hello CSDN\\n");
printf("hello CSDN\\n");
printf("hello CSDN\\n");
fflush(stdout);
close(fd);
return 0;
}
输入重定向
输入重定向与输出重定向原理相同,输出重定向是关闭1号文件描述符,然后再创建一个新的文件。而输入重定向是关闭stdin的0号文件描述符,然后再创建新的文件,然后从打卡的文件中输入数据。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
close(0);
int fd=open("log.txt",O_RDONLY);
if(fd<0) //检测是否打开失败
{
perror("open error\\n");
}
char buff;
while(1)
{
ssize_t size= read(fd,&buff,1);
if(size<0)
{
perror("read error\\n");
return 1;
}
else if(size==0)
{
printf("read end\\n");
break;
}
else
{
printf("%c",buff);
}
}
close(fd);
return 0;
}
dup2函数
既然我们知道了重定向的原理,那么我们现在直接利用dup2函数来直接完成重定向。
函数原型:int dup2(int oldfd, int newfd);
函数功能: 该函数的重定向的原理是将要重定向的文件描述符对应里面的内容拷贝到你想要重定向的任何一个文件描述符下。
函数返回值:
1、dup2如果调用成功,返回newfd。
2、失败返回-1。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);
int fd=open("log.txt",O_RDONLY);
int i= dup2(fd,0);
if(i<0)
{
perror("dip2 error\\n");
}
char buff;
while(1)
{
ssize_t size= read(fd,&buff,1);
if(size<0)
{
perror("read error\\n");
return 1;
}
else if(size==0)
{
printf("read end\\n");
break;
}
else
{
printf("%c",buff);
}
}
close(fd);
return 0;
}
注意: dup2是将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,也就是newfd是oldfd的一份拷贝,最后就会有两份oldfd指向同一个文件,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
FILE
注意这里的FILE与前面文件的小写file不要当成同一个,大写的FILE是C语言提供用来进行I/O操作的,而小写的file是OS系统提供的。
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
我们可以去source insight这里软件中去查看FILE这个结构体内部有一个成员变量叫 _fileno就是文件描述符。
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
软硬链接
硬链接是通过inode引用另外一个文件,而软链接是通过名字引用另外一个文件。
软链接
我们通过ln指令来创建一个软连接。
如下:
ln -s 选项表示软链接,后接要链接的文件名,最后接软链接的文件名。
通过ll -i 命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。软链接就类似于Windows操作系统当中的快
以上是关于linux基础IO的主要内容,如果未能解决你的问题,请参考以下文章