Perk

 

Unix环境高级编程笔记一:文件IO

 

1.文件结构体

1.1printf函数是如何调用标准输出的?

可以用一句话概括:printf函数内部调用一个叫做__vfprintf_internal的函数,在内部将stdout当参数传入。

1.1.1 /usr/include和glibc源码

在/usr/include目录下面有一堆的c语言头文件。如果在ide里面点击查看源码,会跳转到这个目录下查找。不过只有头文件没有实现。要想查看实现,需要自己下载glibc源码。

1.1.2 printf函数源码分析

下面从源码角度分析实现过程:
在vscode里面点击prinf函数,定位的源码位置是/usr/include/bits/stdio2.h
 
 
 
 
 

5
 
 
 
1
__fortify_function int
2
printf (const char *__restrict __fmt, ...)
3
{
4
  return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
5
}
 
 
要是再查看这个__printf_chk发现它只有定义,找不到实现。退一步说,这里的printf也只是在头文件中,那么具体实现在哪里?在linux系统里面c源码已经被打包到/lib/libc.so.6中了。这样我们就看不到源码。要想看源码可以下载glibc源代码。linux系统在安装时默认安装的c语言库就是glibc。
笔者使用的系统是linux的一个发行版manjaro,版本是20.1.2,内核版本是5.10.7.3。/usr/include这个目录包含了很多类库的头文件。类库在安装到系统上时,会把头文件写到这个目录下。linux默认使用的c语言库是glibc。在manjaro系统上可以使用pacman -Q glibc查看版本,ubuntu可以输入命令getconf GNU_LIBC_VERSION。
笔者系统使用的glibc版本为2.32.4。
要想查看c语言源码实现,需要自己下载glibc源码。地址为http://ftp.gnu.org/gnu/glibc/
这里笔者下载了2.32版本,和系统版本是对应的。
打开下载解压后的glibc.xxx/include/bits/stdio2.h文件,非常奇怪,只有一行代码。
 
 
 
 
 

1
 
 
 
1
#include <libio/bits/stdio2.h>
 
 
这里的glibc.xxx/libio/bits/stdio2.h和/usr/include/stdio2.h是一样的,说明系统在安装glibc的时候已经将这些include合并了。
 
printf函数的实现就在glibcxxx/stdio-common/printf.c。同目录下面还有一个printf.h。
printf.c就下面这一个函数。
 
 
 
 
 

12
 
 
 
1
int
2
__printf (const char *format, ...)
3
{
4
  va_list arg;
5
  int done;
6

7
  va_start (arg, format);
8
  done = __vfprintf_internal (stdout, format, arg, 0);
9
  va_end (arg);
10

11
  return done;
12
}
 
 
这里最重要的就是__vfprintf_internal (stdout, format, arg, 0);这一行。
 
 
 
 
 

1
 
 
 
1
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
 
 
这里的FILE *s传入的是stdout,所以printf函数默认是调用了标准输出的。
这里有个非常有意思的细节是stdin,stdout,stderr这个三个io流的定义的注释。C89/C99 say they're macros(宏).  Make them happy。意思是说这三个流是C89/C99标准定义的宏,本来声明变量就行,因为C89/C99标准规定他们是宏,为了满足C语言标准协会的要求,自愿(bei po)定义了名字和内容一模一样的三个宏。
 
 
 
 
 

8
 
 
 
1
/* Standard streams.  */
2
extern FILE *stdin;/* Standard input stream.  */
3
extern FILE *stdout;/* Standard output stream.  */
4
extern FILE *stderr;/* Standard error output stream.  */
5
/* C89/C99 say they're macros.  Make them happy.  */
6
#define stdin stdin
7
#define stdout stdout
8
#define stderr stderr
 
 

1.2 FILE结构体和IO调用流程

FILE声明在glibc.xxx/libio/bits/types/FILE.h
 
 
 
 
 

9
 
 
 
1
#ifndef __FILE_defined
2
#define __FILE_defined 1
3

4
struct _IO_FILE;
5

6
/* The opaque type of streams.  This is the definition used elsewhere.  */
7
typedef struct _IO_FILE FILE;
8

9
#endif
 
 
 
真正的定义在结构体_IO_FILE里面。结构体定义在glibc.xxx/libio/bits/types/struct_FILE.h
ptr表示当前读写指针,base表示起始读写指针位置,end表示结束读写指针位置。
 
 
 
 
 

16
 
 
 
1
//other code...
2
struct _IO_FILE
3
{
4
  int _flags;/* High-order word is _IO_MAGIC; rest is flags. */
5

6
  /* The following pointers correspond to the C++ streambuf protocol. */
7
  char *_IO_read_ptr;/* Current read pointer */
8
  char *_IO_read_end;/* End of get area. */
9
  char *_IO_read_base;/* Start of putback+get area. */
10
  char *_IO_write_base;/* Start of put area. */
11
  char *_IO_write_ptr;/* Current put pointer. */
12
  char *_IO_write_end;/* End of put area. */
13
  char *_IO_buf_base;/* Start of reserve area. */
14
  char *_IO_buf_end;/* End of reserve area. */
15
};
16
//other code
 
 
还有一种实现方式是下面这种,源码来自《C语言程序设计语言》,书中说源码在stdio.h里面,但是在glibc里面是没有这段源码的,即使在最早的glibc1.0版本。这本书的第一版是在1978年。
 
 
 
 
 

7
 
 
 
1
typedef struct _iobuf {
2
int cnt; /* characters left */
3
char *ptr; /* next character position */
4
char *base; /* location of buffer */
5
int flag; /* mode of file access */
6
int fd; /* file descriptor */
7
} FILE;
 
 
 
文件IO的整个调用流程如下图所示:
 
 

1.3 IO缓存类型

C语言IO有三种缓存类型:全缓存、行缓存、无缓存。比较有用的是行缓存。
全缓存:
  •     要求填满整个缓存区后才进行IO系统调用。磁盘文件通常使用全缓存访问。
行缓存:
  •    涉及一个终端(例如标准输入输出)。
  • 行缓存满自动输出
  • 碰到换行符自动输出
无缓存:
  •     标准错误流stderr通常是不带缓存区的,这样可以使错误更快的显示出来
行缓存案例:
 
 
 
 
 

7
 
 
 
1
#include<stdio.h>
2
int main(){
3
  printf("line buf");
4
  while(1){
5
  sleep(1);
6
  }
7
}
 
 
什么都不会输出,如果加上换行符\n,那么会立刻输出。

1.4常用文件操作函数

1.4.1 create函数

先讲这个函数是为了方便讲后面的函数。
 
 
 
 
 

1
 
 
 
1
int creat(const char* path, int mode) 
 
 
第一个参数传路径没什么好说的,第二个参数是重点,用来给定文件的权限,比如如果我想创建一个权限为755的文件,那么mode应该传入0755,注意前面的0,这里不能直接传755,前面必须加0,否则创建的文件权限不对。写法来自《C程序设计语言》,书上的例子就是这样,也没有解释为什么。
下面是一个创建文件的简单例子:
 
 
 
 
 

11
 
 
 
1
#include<stdio.h>
2
#include<stdlib.h>
3
#include<unistd.h>
4
#include<fcntl.h>
5
#define PERM 0755
6
int main(int argc, char const *argv[])
7
{
8
    int fd=creat("cowsay.txt",PERM);
9
    printf("fd=%d",fd);
10
    return 0;
11
}
 
 

 

创建成功fd返回一个大于0的值,失败返回-1.这里有个细节就是创建文件的位置。这里指定的是一个相对位置cowsay.txt。如果使用命令行编译运行的话,创建的文件位置是在同级目录下,如果是在像vscode这种ide,那么位置是由ide决定的,一般默认是项目工程最顶层的目录下。比如源码路径为linux/io/creat.c,其中linux这个目录是项目的根目录,那么cowsay.txt会在linux这个目录下被创建。
如果往cowsay.txt文件里面写入内容,再执行上面的代码,那么写入的内容会被清空。所以creat函数还是非常危险的。需要先判断文件是否存在,不然很可能会丢失数据。但是有可能程序员会忘记写判断文件是否存在,所以还是最好不要使用。

1.4.2 open函数

open函数第一个参数传文件路径,第二个参数传标识位,总共有18个.这里不一一列举。会在例子中穿插说明。
 
 
 
 
 

2
 
 
 
1
#include<fcntl.h>
2
open (const char *__path, int __oflag, ...)
 
 
打开文件之前还是要判断文件是否存在,上面creat函数不足的地方就是创建文件已经存在会清空原来的文件,而使用open函数并指定O_CREAT标识位就不会清空文件。这种方式打开文件,如果文件存在就打开文件,如果文件不存在就会创建文件。必须需要传入权限值,和creat函数的方式一样。这里有个细节,如果文件已经存在,那么即使你传入了权限值,也不会修改文件的权限,还是会以原来的权限打开文件。例如原来的权限是700,你请求的权限是755,那么执行代码后,这个文件的权限还是700.
 
 
 
 
 

12
 
 
 
1
#include<stdio.h>
2
#include<stdlib.h>
3
#include<unistd.h>
4
#include<fcntl.h>
5
#define PERM 0755
6

7
int main(int argc, char const *argv[])
8
{
9
    int fd=open("cowsay.txt",O_CREAT,PERM);
10
    printf("fd=%d\n",fd);
11
    return 0;
12
}
 
 
这里需要注意的是open("cowsay.txt",O_CREAT,PERM);这行代码仅仅是创建文件,即使指定的权限是可读写的,但并没有打开文件,如果这个时候调用write或者read函数,会返回-1。
 
 
 
 
 
 
 
 
 
 
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <unistd.h>
4
#include <fcntl.h>
5
#include <string.h>
6
#define PERM 0755
7
int main()
8
{
9
    int n;
10
    char buf[1024]="this is line one";
11

12
    int fd=open("cowsay.txt",O_CREAT);
13
    printf("fd=%d\n",fd);
14
    int buff_size=strlen(buf);
15
    size_t num=write(fd,buf,buff_size);
16
    printf("num=%d\n",num);//这里会返回-1
17
    exit(0);
18
}
 
 
正确的写法应该是
 
 
 
 
 
 
 
 
 
1
open("cowsay.txt",O_CREAT+O_RDWR);
 
 
O_RWWR就是可读写的意思。
 
还有一个常用的标识位是O_RDONLY,也就是只读。这里可以会有个疑问,上面的O_CREAT方式已经非常完美了,为什么还要用别的O_RDONLY这种方式?这是由业务决定的。并不是所有的文件都需要不存在就创建的。比如业务需求就是只查看一个文件。如果这个文件不存在直接退出并报告文件文件不存在。那么就没必要创建文件。
 
 
 
 
 

11
 
 
 
1
#include<stdio.h>
2
#include<stdlib.h>
3
#include<fcntl.h>
4

5
int main(int argc, char const *argv[])
6
{
7
    int fd=open("cowsay.txt",O_RDONLY);//文件不存在或者没有权限的话fd返回-1
8
    //文件不存在则创建文件,文件的位置
9
    printf("fd=%d\n",fd);
10
    return 0;
11
}
 
 
O_WRONLY也有特定的业务需求,这里先说一个可能读者容易混淆的问题,就是可读是不是一定可写?答案是 不是,另外可写一定可执行。不读怎么写呢?这是个误解,不需要读也可以往文件写内容,也就是不需要知道文件的内容。比如我可以在文件开头结尾插入内容,不需要知道文件里面的内容,这是没有问题的。再来说需求,首先还是要不要判断文件存在的问题。比如业务需求就是不存在就结束,那么就可以不创建,直接结束。
 
 
 
 
 

11
 
 
 
1
#include<stdio.h>
2
#include<stdlib.h>
3
#include<fcntl.h>
4

5
int main(int argc, char const *argv[])
6
{
7
    int fd=open("cowsay.txt",O_WRONLY);//文件不存在或者没有权限的话fd返回-1
8
    //文件不存在则创建文件,文件的位置
9
    printf("fd=%d\n",fd);
10
    return 0;
11
}
 
 
O_RDWR表示可读写,一般的逻辑是先读取内容,在根据内容为条件,写入或者修改内容。一般不需要创建文件,因为文件如果不存在也就没有了判断的条件,也就不需要读写了,直接报告文件不存在,结束程序就行了。

posted on 2021-03-02 17:50  Perk  阅读(415)  评论(0)    收藏  举报

导航