本章主要讲解学习Linux基础IO流的知识
具体详解博文: 文件操作超详解CSDN博客
FILE * fopen(const char* filename, const char* mode);
int fclose (FILE* stream );
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
int fscanf( FILE *stream, const char *format [, argument ]... );
int fprintf( FILE *stream, const char *format [, argument ]...);
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
FILE* fp=fopen("cole.txt","w+");//以写读方式打开文件
if(fp==NULL)
{
perror("fopen");
exit(1);
}
char msg[]="linux so easy!\n";
int len=strlen(msg);
int cnt=5;
while(cnt--)
{
fwrite(msg,len,1,fp);//写入
}
fseek(fp,0,SEEK_SET);//指针回到起始位置
char buf[128];
while(1)
{
ssize_t s=fread(buf,1,len,fp);
if(s>0)
{
buf[s]=0;//设置结束符
fwrite(buf,len+1,1,stdout);
}
else//读取结束
break;
}
fclose(fp);
return 0;
}
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
由此将普通文件和硬件设备管理组织好,所以对于Linux来说:一切皆文件
操作文件,除了上述C接口(当然C++也有接口,其他语言也有),还可以使用系统接口
#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);
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
注:这里的参数选项是依靠不同的比特位来标识对应的功能设定,所以这里的异或操作就是将对应比特位置为1,同时函数也是通过对每个比特位进行与操作检查是否该比特位置为了1
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
#define O_CREAT 0100
注:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open
int close(int fd);
//使用close函数时传入需要关闭文件的文件描述符fd即可,若关闭文件成功则返回0,若关闭文件失败则返回-1
ssize_t write(int fd, const void *buf, size_t count);
//使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中
//如果数据写入成功,实际写入数据的字节个数被返回;如果数据写入失败,-1被返回
ssize_t read(int fd, void *buf, size_t count);
//使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中
//如果数据读取成功,实际读取数据的字节个数被返回;如果数据读取失败,-1被返回
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int fd=open("cole",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
exit(1);
}
char msg[]="i like linux!\n";
int len=strlen(msg);
int cnt=5;
while(cnt--)
{
write(fd,msg,len);
}
close(fd);
fd=open("bit",O_RDWR);
char buf[128];
while(1)
{
ssize_t s=read(fd,buf,len);
if(s>0)
{
buf[s]=0;
write(1,buf,len+1);
}
else
break;
}
close(fd);
return 0;
}
文件描述符就是一个小整数
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));//标准输入
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));//标准输出
write(2, buf, strlen(buf));//标准错误
}
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd1=open("fd1.txt",O_WRONLY|O_CREAT,0644);
int fd2=open("fd1.txt",O_WRONLY|O_CREAT,0644);
int fd3=open("fd1.txt",O_WRONLY|O_CREAT,0644);
int fd4=open("fd1.txt",O_WRONLY|O_CREAT,0644);
int fd5=open("fd1.txt",O_WRONLY|O_CREAT,0644);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
return 0;
}
注:从示例中可见,文件描述符就是从0开始的小整数:默认打开0,1,2,再打开则是从后递增
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果:输出3
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果:关闭0输出0,关闭2输出2
Linux 中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器
注:通常是用文件或命令的执行结果来代替键盘作为新的输入设备,而新的输出设备通常指的就是文件
命令符号格式 | 作用 |
---|---|
命令 > 文件 | 命令的结果输出到文件上 |
命令 >> 文件 | 命令的结果追加输出到文件上 |
命令 < 文件 1 > 文件 2 | 将文件 1 作为命令的输入设备,该命令的执行结果输出到文件 2 中 |
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
close(1);//关闭标准输出
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);//强制输出
close(fd);
exit(0);
}
注:本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1,这种现象叫做输出重定向
从上述示例来看,输出重定向是将进程中的文件指针数组中的标准输出stdout文件给关闭(并非真正关闭,而是将指针数组对应下标的内容置空),再将新打开文件分配到标准输出文件对应的下标上,再输出时,系统不知道文件已经替换,依旧输出到stdout文件对应的数组下标为1的文件上,但是此时文件已经被替换了
#include <unistd.h>
int dup2(int oldfd, int newfd);//将文件地址oldfd替换到newfd上
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("log.txt", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);//输出重定向
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read");
}
printf("%s", buf);
fflush(stdout);
return 0;
}
注:重定向与程序替换是可以同时进行,重定向改变的是进程PCB中的文件指针数组中的文件地址信息,而程序替换则是触发写时拷贝将进程地址空间的代码和数据进行替换,这之间没有影响
系统创建子进程exec替换程序执行cat test.c命令之前,先将标准输出文件关闭,并打开myfile文件(如果不存在则创建,对应的open选项则是O_WRONLY|O_CREAT)
这里大致和输出重定向一样,只不过open的选项改为O_APPEND|O_CREAT
系统创建子进程exec替换程序执行 test.c 命令之前,先将标准输入文件关闭,并打开 mycmd 文件(对应的open选项则是O_RDONLY)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
close(1);//关闭标准输出
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
//fflush(stdout);
close(fd);
exit(0);
}
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的,所以C库当中的FILE结构体内部,必定封装了fd
#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 write
hello printf
hello fwrite
hello printf
hello fwrite
区别:这里 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用),而这与就和fork有关
//在/usr/include/stdio.h
typedef struct _IO_FILE FILE;
//在/usr/include/libio.h
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 di rectly. */
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
};
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
每行包含7列:模式;硬链接数;文件所有者;组;大小;最后修改时间 ;文件名
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
通过读取存储在磁盘上的文件信息,然后显示出来
对于文件操作来说,我们操作的都是在内存打开的文件,而大多数文件都是未打开的文件并且储存在磁盘上,而对于磁盘上的文件OS也需要进行管理,由此就需要文件系统
确定读写信息在磁盘的哪个盘面/柱面/扇区,但是这样的方式并不便于移植,由此我们将磁盘抽象成数组,数组的下标是单调递增不重复的数字,可以直接确定要读写的文件
注:其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复;磁盘分区并格式化后,每个分区的inode个数就确定了
通过遍历inode位图的方式,找到一个空闲的inode,在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中,并将该文件的文件名和inode的映射关系添加到目录文件的数据块中,如果写入内容,需要通过Block Bitmap找到闲置的数据块,将数据写入数据块,并将映射关系写到inode结构中
通过目录文件中的数据块找到文件名及其inode的映射,再找到对应的inode结构,再通过inode结构找到存储该文件内容的数据块,并将数据写入数据块;若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系
将该文件对应的inode在inode位图当中置为无效,将该文件申请过的数据块在块位图当中置为无效,并不真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的,如果再次创建文件及数据,可能将对应的数据块给覆盖,原来的数据也就没有了
目录也是文件,有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等;目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针 注: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。计算机只关注文件的inode号,只有用户才关注,用户需要看到文件名,所以将文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来
注:硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2
创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。而目录创建后,该目录下默认会有两个隐含文件.和…,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2
注:通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件
注:编译时默认是动态编译,加上-static选项则是静态编译
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
注:测试目标文件生成后,静态库删掉,程序照样可以运行
- 动态库是文件,先从磁盘加载到内存上的共享区,并与进程的程序地址空间建立映射关系,由此映射的位置不能影响到进程就需要fPIC
- 具体操作:首先将库文件所在目录的路径存入一个以.conf为后缀的文件当中;然后将该.conf文件拷贝到/etc/ld.so.conf.d/目录下;使用ldconfig命令将配置文件更新