文件I/O总结

2017-12-11  本文已影响0人  FlyingReganMian

1.1 C标准函数与系统函数

C标准是工作在操作系统之上的。比如要执行C标准函数printf函数,printf会调用操作系统提供的应用层接口中的write函数,write函数会调用Linux内核函数的sys_write函数,sys_write 函数会调用驱动层函数,将printf中所要打印的内容显示到显示器上。

C库函数和系统库函数.png

1.2 stdout,stdin,stderr

一个程序运行的时候默认打开了三个文件(或流):stdin(标准输入),stdout(标准输出)和stderr(标准错误)。操作系统会自动将文件描述符0,1,2分配给stdin,stdout,stderr。 因此,用户可以自动分配的文件描述符只能从3开始。操作系统描述符并不是无限的,可以通过“ulimit -n” 来查看,操作系统中文件描述符上限。

其中,0就是stdin,表示输入流,指从键盘输入,1代表stdout,2代表stderr,1,2默认是显示器。

printf("helloworld")其实就是向stdout中输出,等同于fprintf(stdout,"helloworld"),
perro("helloworld")其实就是向stderr中输出,相当于fprintf(stderr,“helloworld”)。

那到底stdout,和stderr有什么区别和作用?

区别一:

 int main()  
 {  
      printf("Stdout1\n");  
      fprintf(stdout,"Stdout2\n");  
      perror("Stder1\n");  
      fprintf(stderr,"Stderr2\n");  
       
      return 0;  
 }  

编译过后,./test,屏幕上是四条输出,如果./test > test.ext ,结果是屏幕上输出两条:Stderr1 和 Stderr2,于是我们可以随便处理我们想要的输出,例如:

./test 1>testout.txt 2>testerr.txt,我们将stdout输出到文件testout.txt中,将stderr输出到testerr.txt文件中;
./test 1>testout.txt ,将stdout输出到文件testout.txt 中,stderr输出到屏幕上;   
./test 2>testerr.txt,将stderr输出到文件testerr.txt中,stdout输出到屏幕上;     
./test > test.txt 2>&1,这是将stdout和stderr重定向到同一文件test.txt文件中。  

区别二

int main(){
    fprintf(stdout,"Hello ");
    fprintf(stderr,"World!");
    return0;
}

输出是:
World!Hello
在默认情况下,stdout是行缓冲的,它的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出到屏幕。

1.3 文件结构

stdin,stdout,stderr其实就是操作系统产生的三个文件。下图为文件结构:

文件结构.png

文件结构体主要包含文件描述符,读写指针位置和缓存区。文件描述符指向磁盘中文件的位置。当读写文件数据的时候,系统会根据文件描述符找到磁盘文件的位置,找到文件后开始向文件中写数据,系统并不会直接将数据写到磁盘上,而是先写到文件缓冲区(大小为8KB)中,当文件缓冲区中的数据大小超过一定阈值或当某项数据停留在缓冲区的时间超过某个时间阈值时,系统会释放缓存区,将数据同步到磁盘上。
这里要强调两点:

  1. 缓冲区类型与调用的函数接口无关,与调用时指定的参数预计默认值有关;
  2. 标准I/O缓存区是针对每个流的(FILE *fp),而不是针对I/O函数的。

缓冲区可以分为:用户程序缓冲区、C标准I/O缓冲区 和 内核I/O缓冲区

  1. 用户程序缓冲区
      应用程序级别的数据空间,局部或者全局变量等;

  2. C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。

    1. 全缓冲:如果缓冲区写满了就写回内核。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
    2. 行缓冲:如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。
    3. 无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

3.内核I/O缓冲区

当用户要将“hello world”写入磁盘时,调用C标准库函数fwrite,将“hello world”写到C标准库层的缓冲区中,经过一段时间或者该缓冲区已满或者此时用户调用fflush函数,系统将C标准库缓冲区中内容写到内核缓冲区中,内核中有一个守护进程会每隔一段时间将内核缓冲区中数据同步到磁盘中。
  事实上,进程并不关心数据是写到了磁盘上还是内核缓冲区上,进程A写到内核缓冲区中的数据进程B也可以读到,而C标准库缓冲区不具备这个特性。这是因为C标准库缓冲区是在应用层,在应用层进程和进程之间是相互隔离的,所以进程B读不到进程A的C标准库缓冲区。

1.4 PCB

1.4.1 task_struct结构体

PCB,Process Control Block,进程控制块。运行中的程序被称为进程。操作系统使用task_struct结构体来表示一个进程。可以在/usr/src/kernels/2.6.18-194.el5-xen-i686/include/linux/sched.h找到关于task_struct的定义。

image.png

操作系统为了管理进程,内核会为每一个进程创建了一个PCB。PCB会维护一个files_struct指针.files_sturct指针会指向一个文件描述符表。 文件描述符表里面存的就是文件描述符,每一个文件描述符指向一个磁盘上的文件。
当一个进程打开一个文件,例如,File *file = fopen("1.txt").fopen打开一个文件“1.txt”,会返回一个File结构的指针file。file中就包含了所打开文件的文件描述符,该文件描述符就被在PCB中的files_struct中。

1.5 open/close函数

从1.1节可知,一个进程默认打开三个文件描述符:stdin文件描述符0,stdout文件描述符1,stderr文件描述符2。
新打开文件返回的文件描述符表中国未使用的最小文件描述符。因此,新打开文件返回的文件描述符至少是3.

  1. 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);
    返回值:成功返回新分配的文件描述符,出错返回-1并设置errno

pathname参数表示文件路径,flags表示以什么样的方式打开文件,例如:

    O_RDONLY 只读打开
    O_WRONLY 只写打开
    O_RDWR 可读可写打开

打开/创建文件时,至少得使用上述三个常量中的一个。以下常量是选用的:

    O_APPEND  每次写操作都写入文件的末尾
    O_CREAT  如果指定文件不存在,则创建这个文件
    O_EXCL  如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
    O_TRUNC  如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
    O_NOCTTY  如果路径名指向终端设备,不要把这个设备用作控制终端。
    O_NONBLOCK  如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

mode参数指定了文件权限,比如0644表示-rw-r-r–,要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。例如,用gcc编译生成一个可执行文件时,创建权限是0777,而最终的文件权限是0777 & ∼022 = 0755。在Linux终端中输入umask可以查看umask掩码。  

补充说明一下fopen函数和open函数区别

  1. fopen 系列是标准的C库函数;open系列是 POSIX 定义的,是UNIX系统里的system call。
    也就是说,fopen系列更具有可移植性;而open系列只能用在 POSIX 的操作系统上。
  2. 使用fopen 系列函数时要定义一个指代文件的对象,被称为“文件句柄”(file handler),是一个结构体;而open系列使用的是一个被称为“文件描述符” (file descriptor)的int型整数。
  3. fopen 系列是级别较高的I/O,读写时使用缓冲;而open系列相对低层,更接近操作系统,读写时没有缓冲。由于能更多地与操作系统打交道,open系列可以访问更改一些fopen系列无法访问的信息,如查看文件的读写权限。这些额外的功能通常因系统而异。
  4. 使用fopen系列函数需要"#include <sdtio.h>";使用open系列函数需要"#include <fcntl.h>" ,链接时要之用libc(-lc)
  1. close函数:关闭一个已经打开的文件

     #include <unistd.h>
     int close(int fd);
     返回值:成功返回0,出错返回-1并设置errno
    

    当一个进程终止时,内核会对该进程所有未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。

    由open返回的文件描述符一定是该进程尚未使用的最小描述符。现在,首先调用close关闭文件描述符1,然后调用open打开一个常规文件,则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用printf就不会打印到屏幕上,而是写到这个文件中了。

1.6 read/write 函数

  1. read函数:从打开的设备或文件中读取数据。

     #include <unistd.h>
     ssize_t read(int fd, void *buf, size_t count);
     返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0
    

参数count是请求读取的字节数,读取的数据保存在buf中。其中buf中读写位置是保存在内核中的,而C标准I/O库的函数读写位置是保存在用户空间I/O缓冲区中的。
有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:
1. 读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个
2. 字节而请求读100个字节,则read返回30,下次read将返回0。
3. 从终端设备读,通常以行为单位,读到换行符就返回了。
4. 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数

  1. write函数向打开的设备或文件中写数据。

     #include <unistd.h>
     ssize_t write(int fd, const void *buf, size_t count);
     返回值:成功返回写入的字节数,出错返回-1并设置errno
    

1.7 阻塞读终端

阻塞(block),是当一个进程调用一个阻塞的系统调用函数,并且没有获取到所需要的资源时,该进程被设置为睡眠状态。
下面以阻塞读终端代码为例,展示一下阻塞。

#include <unistd.h>
#include <stdlib.h>
int main(void)
{
    char buf[10];
    int n;
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
        perror("read STDIN_FILENO");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
}

gcc编译后,执行结果如下:

$./a.out  
hello
hello
$./a.out  
hello linux
hello linu$ x 
bash:x:command not found

第一次执行结果很正常。现在分析一下第二次执行结果:

  1. Shell进程创建a.out进程,a.out进程开始执行,Sell进程睡眠等待a.out进程退出。
  2. a.out进程调用read函数,陷入阻塞,等待输入。直到终端设备输入换行符read函数返回,此时read只读取了10个字符,剩余字符x依然在内核的终端设备输入缓冲区内。
  3. a.out进程调用write函数,打印读取的字符并退出。
  4. Shell进程恢复,Shell进程从终端读取用户输入的命令。Shell进程读到了字符x和换行符。
  5. Shell进程在环境变量(echo $PATH)查找是否存在x命令,发现不存在x命令,返回command not found命令。

1.8 非阻塞读终端

如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"

int main(void)
{
    char buf[10];
    int fd, n;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
    if(fd<0) {
        perror("open /dev/tty");
        exit(1);
    }
    tryagain:
    n = read(fd, buf, 10);
    if (n < 0) {
        if (errno == EAGAIN) {
            sleep(1);
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            goto tryagain;
        }
        perror("read /dev/tty");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    close(fd);
    return 0;
}   

1.9 lseek和fseek

  1. lseek()函数:移动文件的读写位置
    #include <sys/types.h> 
    #include <unistd.h>
    off_t lseek(int fildes, off_t offset, int whence);

每次打开文件都有一个读写位置,记录读写文件是从哪个地方开始读写。例如,通产打开文件,读写头的位置是0,表示文件头,读写多少个字节,读写头位置向后移动多少个字节。但是,如果以O_APPEND方法打开,每次读写头的位置在文件末尾。

**参数whence**:  
1. SEEK_SET 参数offset 即为新的读写位置.  
2. SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
3. SEEK_END 将读写位置指向文件尾后再增加offset 个位移量    

返回值:当调用成功时则返回目前的读写位置, 也就是距离文件开头多少个字节. 若有错误则返回-1, errno 会存放错误代码.

另外,使用lseek扩展一个文件时,必须加上一个此操作,才能真正扩展文件大小。   

    int main()
    {
        int fd = open("hello",O_RDWR);

        lseek(fd,0x1000,SEEK_SET);
        write(fd,"q",1);

        close(fd);
    }
  1. fseek()函数:移动文件流的读写位置
  2.  #include <stdio.h> 
     #include <unistd.h>
     int fseek(FILE * stream, long offset, int whence);
    

参数含义和lseek相同。
返回值:当调用成功时则返回0, 若有错误则返回-1, errno 会存放错误代码.
fseek()不像lseek()会返回读写位置, 因此必须使用ftell()来取得目前读写的位置.

1.10 fcntl

fcntl函数可以改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志。

头文件:

#include <unistd.h>

#include <fcntl.h>

函数原型:          

int fcntl(int fd, int cmd);

int fcntl(int fd, int cmd, long arg);         

int fcntl(int fd, int cmd, struct flock *lock);

操作类型由cmd决定。cmd可取如下值:

F_DUPFD:复制文件描述符
F_DUPFD_CLOEXEC:复制文件描述符,新文件描述符被设置了close-on-exec
F_GETFD:读取文件描述标识
F_SETFD:设置文件描述标识
F_GETFL:读取文件状态标识
F_SETFL:设置文件状态标识
F_GETLK:如果已经被加锁,返回该锁的数据结构。如果没有被加锁,将l_type设置为F_UNLCK
F_SETLK:给文件加上进程锁
F_SETLKW:给文件加上进程锁,如果此文件之前已经被加了锁,则一直等待锁被释放。

1.11 ioctl

ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

#include <sys/ioctl.h>
int ioctl(int d, int request, ...);

d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于request。

以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
    struct winsize size;
    if (isatty(STDOUT_FILENO) == 0)//判断文件描述词是否是为终端机
        exit(1);
    if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
        perror("ioctl TIOCGWINSZ error");
        exit(1);
    }
    printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
    return 0;
}
上一篇下一篇

猜你喜欢

热点阅读