一切皆文件

“一切皆文件”是Linux最出名的设计之一,也就是说,在Linux下,所有的内容都是以文件的形式来保存和管理的,普通文件是文件,目录是文件(window中叫文件夹),硬件设备如键盘/鼠标/U盘等等是文件,就连套接字/网络通信等都被视为文件。

在Linux中,文件大致可以分为以下几种:

  1. 普通文件
    像html、css、txt、mp4等可以直接拿来使用的文件都叫做普通文件,Linux的用户可以根据访问权限的不同对这些文件进行查看、编辑、写入等操作。
  2. 目录文件
    linux中吧目录也视为文件,目录文件包含了此目录中各个文件的文件名以及指向这些文件的指针,打开目录等同于打开目录文件,只要你有权限,可以随意访问目录中的任何文件。这里对于习惯使用windows的用户来说可能难以理解,因为在windows中,“文件”和“文件夹”是两个概念。
  3. 字符设备文件和块文件设备
    这些文件代表了linux的设备,如磁盘、串口等等,字符设备文件和块文件设备一般存放于 /dev/ 这个目录下。
    linux中的所有设备,要么是块设备文件,要么是字符设备文件。
  4. 套接字文件
    套接字文件一般在 /var/run/ 目录下,用于进程间的网络通信。
  5. 符号链接文件
    软连接,类似于windows里边的快捷方式。
  6. 管道文件
    主要用于进程间通信。

I/O的区别

IO可以根据是否带有缓冲区分为两类:

  1. 文件IO(不带有缓冲)
  2. 标准IO(带有缓冲)

标准IO

标准IO是指ANSI C(ANSI C是美国国家标准协会(ANSI)对C语言发布的标准)中定义的用于I/O操作的一系列函数。
只要操作系统中安装了C库,标准I/O函数就可以调用,换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要修改就可以在其它操作系统下编译运行,具有更好的可移植性。

文件IO

Linux中做文件IO最常用到的5个函数是: open , close , read, writelseek ,不是ISO C的组成部分,这5个函数是不带缓冲的IO,也即每个 readwrite 都调用了内核的一个系统调用。

  1. #include <fcntl.h>
  2. #include <unistd.h>
  3. int open(const char *pathname, int oflag, ... /* mode_t mode */);/* 成功返回文件描述符, 失败返回-1 */
  4. int close(int filedes);/* 成功返回0, 失败返回-1 */
  5. off_t lseek(int filedes, off_t offset, int whence);/* 成功返回新的文件偏移量,出错返回-1 */
  6. ssize_t read(int filedes, void *buf, size_t nbytes);/* 成功则返回读取到的字节数,若已到文件的结尾返回0,出错返回-1 */
  7. ssize_t write(int filedes, void *buf, size_t nbytes);/* 成功则返回写入的字节数,出错返回-1 */

既然一切都是文件那么我们就可以使用一套API来操作linux的一切,而 ,它将数据的输入输出看作是数据的流入和流出,这样不管是磁盘文件或者是物理设备(打印机、显示器、键盘等),都可看作一种流的源和目的,视他们为同一种东西,而不管其具体的物理结构,即对他们的操作,就是数据的流入和流出。

根据百度百科的定义:数据流(data stream)是一组有序,有起点和终点的字节的数据序列。

对于流的概念其实很好理解,比如我们有一个水池(这个水池就是一个文件),那么我们给水池中灌水或者从放水(水就是数据),需要一根水管,那么这根水管就是 ,所以:

  1. 往里灌水(写文件)时,对于水管(流)来说,是“将水管里的水输出到水池里”,此时对于流来说是输出流
  2. 往外排水(读文件)时,对于水管(流)来说,是“将水池里的水输入到水管里”,此时对于流来说是输入流

这里需要站在水管的角度去理解输入与输出流。

流的分类

流可分为两大类,即 文本流(text stream)二进制流(binary stream)

文本流:

所谓文本流是指在流中流动的数据是以字符形式出现。在文本流中,输入的时候 \n被换成回车 CR 和换行 LF 的代码 0DH0AH。而当输出时,则把 0DH0AH本换成 \n

二进制流:

二进制流是指流动的是二进制数字序列,若流中有字符,则用一个字节的二进制ASCII码表示,若是数字,则用一个字节的二进制数表示。

例如:2001这个数,在文本流中用其ASCII码表示为:‘2’ ‘0’ ‘0’ ‘1’,共占4字节。 但是在二进制流中为其二进制形式:00000111 11010001 用十六进制就是07D1。只占两字节。

标准数据流

在Linux中,一个程序启动时,将会自动开启三个数据流通道:标准输入流、标准输出流、标准错误流:

  • 标准输入流(stdin: standard input,)
  • 标准输出流(stdout: standard output)
  • 标准错误流(stderr: standard error)

对应的文件描述符分别是:

  • stdin:0
  • stdout:1
  • stderr:2

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4。

程序运行的时候从输入流读取数据,作为程序的输入,程序运行过程中输出的信息被传送到输出流,类似的,错误信息被传送到错误流。

类型 编号 说明
stdin 0 标准输入
stdout 1 标准输出
stderr 2 标准错误

标准输入流

标准输入流默认是从 键盘输入的信息。

下面我们通过标准输入流获取键盘输入的内容:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int i;
    printf("please input a number:");
    fscanf(stdin, "%d", &i);
    printf("the number is:%d", i);
    return 0;
}

上面的程序中我们使用了 fscanf 函数,这个函数有三个参数,第一个参数就是需要一个输入流,这里我们选择了标准输入流。

这里我们看一下我们平常使用的 scanf 和这里的 fscanf 函数的区别:

Linux标准IO - 图1

可以看到,scanf 函数其实已经帮我们设定好了输入的是标准输入流,而 fscanf 可以让我们自己定义输入的流是什么。

再例如,在linux中,我们使用 passwd命令来修改密码:

Linux标准IO - 图2

在修改密码的过程中,你需要从键盘输入当前的密码和新的密码。标准输入流就是键盘,你输入的密码从标准输入流传送到 passwd程序。

标准输出流

输出流分为标准输出流和标准错误流,默认情况下,它们都会输出到屏幕。

以编译程序为例子,编译过程中的调试信息(标准输出流)和编译发生的错误(标准错误流)都会被显示在屏幕上。

下面的例子就是通过标准输入流输出一个 Hello World :

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int i;
    fprintf(stdin,"please input a number:");
    fscanf(stdin, "%d", &i);
    fprintf(stdin,"the number is:%d", i);
    return 0;
}

同上面一样,我们这里使用 fprintf函数的第一个参数来定义输出的流的类型。

标准错误流

与标准输出流类似,标准错误流默认也会输出到屏幕上。例如:

Linux标准IO - 图3

流的重定向

在Linux中,标准输入流默认来自键盘输入,标准输出流和标准错误流默认发送到屏幕。在必要的时候,可以对修改输入流的来源、修改输出流的目的,这就是重定向。

常用的重定向的符号:

  1. >: 将标准输出流重定向到文件(清空文件后写入)。
  2. >>:将标准输出流重定向到文件(追加写入)。
  3. <:将文件作为命令的标准输入流

标准输出流重定向

我们使用 ls -l 命令来将输出的流重定向到一个文件中:

ls -l > test.txt

上面命令执行后,我们在屏幕上看不到任何的输出,这是因为我们的输出被重定向到了文件。

此时我们查看一下 test.txt 文件,可以看到本应该输出到屏幕的内容被输出到了文件中:

Linux标准IO - 图4

输入流重定向

下面我们通过重定向输入流来把上面生成的文件的内容复制到另一个文件中:
Linux标准IO - 图5

错误流重定向

默认情况下,重定向符号 >>>只能对标准输出流进行重定向,而没有对标准错误流重定向。
例如,使用 rm命令删除根目录时,会报错无法删除:

Linux标准IO - 图6

尝试将输出写入到一个文件中:Linux标准IO - 图7

可以看到,错误信息依旧被显示到屏幕上,查看 test3.txt时,发现什么内容都没有写入。
这是因为,重定向符号“>”默认只会重定向标准输出流,而不会重定向标准错误流。所以标准错误流依旧按照默认方式输出到屏幕上。

如果想对错误流进行重定向,可以使用以下语法:

rm / 2> test3.txt

在重定向符号前加上了一个 2,这个 2就是标准错误流的文件描述符。
命令执行后没有任何输出,在文件 test3.txt 的内容中,可以看到:rm: 无法删除'/': 是一个目录

缓冲

为什么设置缓冲

  1. 从效率的角度考虑,避免频繁地呼叫系统调用(read、write);其次缓冲区大小的设置在不同OS上是有技术的,标准库为我们做了优化选择。
  2. 从安全角度考虑:用户态到内核态的切换。频繁的切换是不安全的。

系统自动的在内存里面为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区,在一起送到磁盘里面。从内存向磁盘读取数据,则一次从磁盘文件中将一批数据读到内存缓冲区中,然后再从缓冲区中将数据送到程序的数据区。

如果使用标准IO的话,会现将数据写到缓冲区中,然后等到缓冲区满了,或者刷新了缓冲区,才整体将这些数据写入磁盘。只执行一次写操作即可,一次数据挪动即可。

缓冲的分类

标准IO函数是根据文件流关联的设备类型,会选择采用何种缓冲区的操作方式。分类如下:

  • 全缓冲区
    这种缓冲区要求填满整个缓冲区后才进行I/O 系统调用操作。对于磁盘文件通常使用全缓冲区访问。第一次执行I/O 操作时,ANSI标准的文件管理函数通过调用 malloc 函数获得需使用的缓冲区。linux默认大小为4096字节。有两种方式可以刷新缓冲区:
    1. ffush()
    2. 缓冲区满
  • 行缓冲区
    在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O系统调用操作。当流涉及一个终端时(例如标准输入和标准输出),使用行缓冲区。因为标准I/O库收集的每行的缓冲区长度是固定的,只要填满了缓冲区,即使还没有遇到换行符也将执行I/O 系统调用操作。默认行缓冲区大小为1024 字节。有三种方式可以刷新缓冲区:
    1. ffush()
    2. 缓冲区满
    3. 遇到\n
  • 无缓冲区
    标准I/O 库不对字符进行缓存。如果用标准I/O 函数写若干字符到不带缓冲区的流中,则相当于用 write系统调用函数将这些字符写至相关联的打开文件。标准出错流stderr 通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

注意:

  • 缓冲区是通过malloc申请的空间
  • 申请的实际,是发生I/O操作的时候
  • 进程结束会默认刷新缓冲区。

下面我们通过一个例子来看一下标准IO的缓冲机制:

#include "stdio.h"
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char a = 'a';
    if (argc != 2)
    {
        printf("Usage:%s Number\n", argv[0]);
        return 0;
    }
    for (int i = 0; i < atoi(argv[1]); i++)
    {
        printf("%c", a);
    }
    while (1)
    {
    }
    return 0;
}

上面的代码中,我们通过命令行接受一个参数,然后循环使用 printf 来打印我们定义好的字符,也就是一个a,在打印完成后,使用一个死循环来让程序不退出,下面请看实际的运行情况:

Linux标准IO - 图8

标准IO编程

流的打开

使用标准I/O打开的函数有 fopen(),fdopen(),freopen()它们可以用不同的模式打开文件,每一个都返回一个指向FILE的指针。 此后,对文件的读写都通过这个FILE指针来进行。

  • fopen():可以指定打开文件的路径和模式;
  • fdopen():可以指定打开文件的描述符和模式;
  • freopen():可以指定打开的文件和模式,还可以指定特定的 I/O 流;

fopen函数

#include<stdio.h>
FILE *fopen(const char *filename, const char *mode);

fopen库函数打开由filename参数指定的文件,并把它与一个文件流关联起来。fopen成功时返回一个非空的 FILE*指针,失败时返回NULL值。

参数说明:

  • filename:指定打开的文件名
  • mode:指定打开文件的方式,可能是一下字符串中的一种:
    Linux标准IO - 图9
  1. 字母b表示文件是一个二进制文件,但是Linux并不区分文本文件和二进制文件,而是把所有文件都看作为二进制文件。
  2. Linux系统可用的文件流数量和文件描述符一样都是有限制的,实际的限制由头文件stdio.h中定义的POPEN_MAX定义,它的值至少为8,Linux系统通常为16。
  3. 当用户进程运行时,系统会自动打开 3 个流: 标准输入流stdin;标准输出流stdout;标准错误流 stderr

流的关闭

关闭流的函数为 fclose();该函数将流的缓冲区的数据全部写入文件中,并释放相关资源。

fclose函数

#include<stdio.h>
int fclose(FILE *stream);
  1. fclose函数用于关闭一个文件流,并使所有尚未写出的数据都写出
  2. 因为stdio库会对数据进行缓冲,所以使用fclose是很重要的。如果程序需要确保数据已经全部写出,就应该调用fclose函数。当程序正常结束时,会自动对所有还打开的文件流调用fclose函数。

错误信息

标准 I/O 函数执行时如果出现错误,会把错误码保存在全局变量 errno 中,程序员可以打印错误信息,处理错误的相关函数:

  • perror()
  • strerror()

perror函数

#include <stdio.h>  
void perror(const char *s);

其实,perror() 函数的使用还是比较简单的,举例说明:

#include "stdio.h"

int main(int argc, char *argv[])
{
    FILE *fp; //指定流指针
    if (NULL == (fp = fopen("a.txt", "r")))
    {
        perror(" fail to open");
        return -1;
    }
    fclose(fp);
}

上面的例子中,我们打开 a.txt这个文件,但是这个文件并不存在,所以输入结果如下:

Linux标准IO - 图10

strerror函数

#include <stdio.h>  
char *strerror (int __errnum);

举例来说明:

#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    FILE *fp; //指定流指针
    if (NULL == (fp = fopen("AA.txt", "r")))
    {
        printf(" fail to fopen: %s\n", strerror(errno));
        return -1;
    }

    fclose(fp);
    return 0;
}

上面的例子中,我们打开 AA.txt这个文件,但是这个文件并不存在,所以输入结果如下:

Linux标准IO - 图11

流的写入

按照字符(字节)输入

字符输入 / 输出函数一次仅读写一个字符。

字符输入函数原型:

  1. int getc ( FILE * stream);
  2. int fgetc ( FILE * stream);
  3. int getchar ( void );

getc()fgetc()从指定的流中读取一个字符(节),getchar() 从 stdin 中读取一个字符(节)。

按照字符(字节)输出

字符输入函数原型:

  1. int putc ( FILE * stream);
  2. int fputc ( FILE * stream);
  3. int putchar ( void );

putc()fputc()从指定的流中读取一个字符(节),putchar() 向 stdout 中输出一个字符(节)。

例如:

#include <string.h>

int main(int argc, char *argv[])
{
    int c;
    while (1) //循环
    {
        c = fgetc(stdin);            // 从键盘读取一个字符
        if (('0' < c) && ('9' >= c)) // 字符判断是否为数字
        {
            fputc(c, stdout); // 满足标准输出
        }
        else if ('\n' == c) // 当遇到‘\n’时跳出
        {
            break;
        }
    }
    return 0;
}

运行是输入 123456dwasdwa ,输出如下:

Linux标准IO - 图12

按照行输入

行输入/输出函数一次操作一行。

行输入函数总结如下:

  1. char * gets( char * s);
  2. char * fgets( char *s, int size, FILE* stream);

注意:

  1. gets()函数不安全,容易造成缓冲区溢出 ,不建议使用。(原因:由于gets()不检查字符串string的大小,必须遇到换行符或文件结尾才会结束输入,因此容易造成缓存溢出的安全性问题,导致程序崩溃,可以使用fgets()代替)。
  2. fgets()从指定的流中读取一个字符,当遇到换行符 \n 时,会读取 \n 或者读取sizee -1个字符后返回。
  3. fgets()不能保证每次都能读取到一行。

行输出函数

  1. `int puts( const char * s);``
  2. `int fputs( const char _ s, FILE _ stream);

指定大小为单位读写文件

在文件流被打开以后,可对文件流按照指定大小为单位进行读写操作。

函数:

  1. fread()
  2. fwrite();

fread函数:

#include<stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

fread函数用于从一个文件流里读取数据。返回值是成功读到数据缓冲区里的记录个数(而不是字节数)。

参数说明:

  • ptr:指定数据缓冲区
  • size:指定每个元素的大小,以字节为单位
  • nitems:指定要读取元素的个数,每个元素的大小为size字节
  • stream:指定要读取的文件流指针

fwrite函数:

#include<stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);

fwrite函数用于从指定的数据缓冲区里取出数据记录,并把它们写到输入流中。返回值是成功写入的记录个数。

参数说明:

  • ptr:指定数据缓冲区
  • size:指定每个元素的大小,以字节为单位
  • nitems:指定要写入元素的个数,每个元素的大小为size字节
  • stream:指定要写入的文件流指针

流的定位

每个打开的流内部都有一个当前读写位置。留在打开时,当前读写位置为 0,表示文件的开始位置,每读写一次后,当前读写位置自动增加实际读写的大小,在读写流之间可先对流进行定位,即移动到当前指定的位置再进行操作。

  1. fseek()
  2. ftell()

fseek函数:

#include<stdio.h>
int fseek(FILE *stream, long int offset, int whence);

fseek函数在文件流里为下一次读写操作指定位置。返回值是一个整数:0表示成功,-1表示失败并设置errno指出错误。

参数说明:

  • stream:指定文件流指针
  • offset:指定偏移量
  • whence:指定偏移量的用法,可选下列值之一:
    Linux标准IO - 图13

流的刷新

有时候我们需要手动刷新缓冲区,那么可以使用下面函数实现:

  1. fflush()

fflush函数

#include<stdio.h>
int fflush(FILE *stream);

fflush函数把文件流里的所有未写出的数据立即写出。调用 fclose函数隐含执行了一次 flush操作,所以不必在调用 fclose之前调用 fflush

格式化输出流

格式化输出流包含以下函数:

  1. printf()
  2. sprintf()
  3. fprintf()
#include<stdio.h>
int printf(const char *format, ...);
int sprintf(const *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
  • printf函数将输出送到标准输出流。
  • sprintf函数把输出加上一个结尾空字符送到字符串s中。
  • fprintf函数把输出输送到指定的文件流中。

format为输出格式类型,通常有以下几种常用的转换控制符:

转换控制符 说明
%d,%i 以十进制格式输出一个整数
%o,%x 以八进制或十六进制格式输出一个整数
%c 输出一个字符
%s 输出一个字符串
%f 输出一个单精度浮点数
%e 以科学计数法输出一个双精度浮点数
%g 以通用格式输出一个双精度浮点数

格式化输入流

格式化输入流的函数有:

  1. scanf()
  2. fscanf()
  3. sscanf()
#include<stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *s,const char *format,. ...);
  • scanf函数从标准输入流中读入数据
  • fscanf函数从指定的文件流中读入数据
  • sscanf函数从指定的字符串中读入数据。

format为输入格式类型,通常有以下几种常用的转换控制符:

转换控制符 说明
%d 读取一个十进制整数
%o,%x 读取一个八进制或十六进制的整数
%c 读取一个字符
%s 读取一个字符串
%f 读取一个单精度浮点数
%e 读取一个科学计数法表示的双精度浮点数
%g 读取一个通用格式表示双精度浮点数
%[] 读取一个字符集合
%% 读取一个%字符

注意:

  1. 使用%c空字符从输入中读取一个字符,它不会跳过起始的空白字符
  2. 关于scanf的安全性问题,由于scanf系列函数可以接受任意的键盘输入,如果输入的长度超出了应用给定的缓冲区,就会覆盖其他数据区,造成缓冲区溢出,而溢出的数据可能覆盖其他内存空间的数据,造成程序崩溃。