C标准I/O库函数与Unbuffered I/O函数

一、C标准I/O库函数、Unbuffered I/O函数

1. C标准I/O库函数是如何用系统调用的

  fopen(3)

    调用open(2)打开制定的文件,返回一个文件描述符(一个int类型的编号),分配一个FILE结构体,其中包含该文件的描述符、I/O缓冲区和当前读写位置等信息,返回这个FILE结构体的地址。

  fgetc(3)

    通过传入的FILE *参数找到该文件的描述符、I/O缓冲区和当前读写位置,判断能否从I/O缓冲区读到下一个字符,如果能就直接返回该字符,否则调用read(2)把文件描述符传进去,让内核读取该文件的数据到I/O缓冲区,然后返回下一个字符。(对于C标准I/O    来说打开的文件由FILE *指针表示,对于内核来说,打开的文件由文件描述符标示,文件描述符从open系统调用获得,在使用read、write、close系统调用时都需要传文件描述符。)

  fputc(3)

    判断该文件的I/O缓冲区是否有空间再存放一个字符,如果有则直接保存在I/O缓冲区中并返回,如果I/O缓冲区中已满就调用write(2),让内核把I/O缓冲区的内容写回文件。

  fclose(3)

    如果I/O缓冲区中还有数据没写回文件,就调用write(2)写回文件然后再调用close(2)关闭文件,释放FILE结构体和I/O缓冲区。

  open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O)函数,用户程序在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层的Unbuffered I/O函数,那个各自使用场景是什么呢?

  • 用Unbuffered I/O函数每次读写都要进内核,调一个系统调用比调一个用户控件的函数要慢很多,所以在用户程序开辟I/O缓冲区还是必要的,用C标准I/O库函数比较方便。
  • 用C标准I/O库函数要时刻注意I/O缓冲区和实际文件有可能不一致,在必要时调用fflush(3)。
  • UNIX的传统是Everything is a file,I/O函数不仅可以读写文件还可以读写设备。在读写设备时通常是不希望有缓冲的。比如网络设置的读写就希望是实时读写。

  C标准库函数是C标准的一部分,而Unbuffered I/O函数是UNIX标准的一部分。只有在UNXI平台上才能用Unbuffered I/O函数,windows上不行。

2. 文件描述符

  每个进程在linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符(Process Descriptor),而在操作系统理论中称为进程控制块(PCB, Process Control Block)。task_struct中有一个指针指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针,如下图所示:

  用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符,用int型变量保存。当调用open打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符边项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read或write,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。

  程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。在C标准中分别用FILE *指针stdin、stdout、stderr表示。这三个文件的描述符分别是0、1、2,保存在相应的FILE结构体中。头文件unistd.h中有如下的宏定义来表示这三个文件描述符:

#define STDIN_FILENO  0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

二、open/close

 1. open

  open函数可以打开或创建一个文件。

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

  在Man Page中open函数有两中形式,一种带2个参数,一种带3个参数,在C代码中open函数的声明是这样的:

int open(const char *pathname, int flags, ...);

  最后的可变参数可以是0个或1个,由flags参数中的标志位决定。

  pathname参数是要打开或创建的文件名,可以是相对路径也可以是绝对路径。flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的定义都以O_开头,表示or

  必选项:以下三个常数中必须指定一个,且仅允许指定一个。

  • O_RDONLY 只读打开
  • O_WRONLY 只写打开
  • O_RDWR 可读可写文件

  以下选项可以同时制定0个或多个,和必选项按位或起来作为flags参数,可选项有很多,以下是其中一部分可选项:

  • O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
  • O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示文件的访问权限
  • O_EXCL 若同时指定了O_CREAT,并且文件已存在,则出错返回。
  • O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断为0字节。
  • O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)。

  open函数与C标准I/O库的fopen函数有些细微的区别

  • 以可写的方式fopen一个文件时,如果文件不存在则自动创建,而open需要制定O_CREAT才会创建文件,否则文件不存在就出错返回。
  • 以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。

  第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r--r--,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示。注意:文件权限由open的mode参数和当前进程的umask掩码共同决定。

  Shell进程的umask掩码可以用umask命令查看:

$ umask
0022

  用touch命令创建一个文件时,创建权限是0666,而touch进程继承了Shell进程的umask掩码,所以最终的文件权限是0666&~022 = 0644

2. close

  close函数关闭一个已打开的文件:

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

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

  由 open 返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符0、1、2,因此第一次调用 open 打开文件通常会返回描述符3,再调用 open 就会返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重定向的功能。例如,首先调用 close 关闭文件描述符1,然后调用 open 打开一个常规文件,则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用 printf 就不会打印到屏幕上,而是写到这个文件中了。

三、 read/write

1. read

  read函数从打开的设备或文件读取数据

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

  参数count是请求读取的字节数,读上来的数据保存在缓冲buf中,同时文件的当前读写位置向后移。这个读写位置和使用C标准库时的读写位置有可能不同。返回值类型为ssize_t表示有符号的size_t,这样既可以返回正的字节数(正数)、0(到达文件末尾)也可以返回负值-1(出错)。read返回时,返回值说明了buf中前多少字节是刚读上来的。有些情况下实际读到的字节数(返回值)会小于请求读的字节数count,例如:

  • 读常规文件时,在读到count字节之前就到达文件末尾了。
  • 从终端设备读,通常以行为单位,读到换行符就返回了。
  • 从网络读数据,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数。

2. write

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

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

写常规文件时,write的返回值通常等于请求写的字节数,而向终端设备或网络写则不一定。

3. 阻塞

  读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的。如果一直没有数据就一直阻塞在那里。写操作同理。

  当进程调用一个阻塞的系统函数时,该进程被置于睡眠状态,这时内核调度其他进程运行,直到该进程等待的事件发生了(比如网络上接收到了数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行状态。在linux内核中,处于运行状态的进程分为两种情况:

  • 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
  • 就绪状态。该进程不需要等待什么事件发生,随时可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。

  如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。

四、lseek

lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

参数offset和whence的含义和fseek函数完全相同。对于whience的设置有三种形式,SEEK_SET,SEEK_CUR,SEEK_END,和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。

若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏移量:

off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);

设备一般是不可以设置偏移量的。如果设备不支持lseek则lseek返回-1,并将errno设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返回-1。要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

五、fcntl

  STDIN_FILENO在程序启动时已经被自动打开,所以我们要改变STD_FILENO的打开方式(比如设置O_NONBLOCK)必须用open函数重新打开。另外一种方法就是用fcntl函数改变一个一打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志,而不必重新open文件。

#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);

  这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd参数。

  下面的程序使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性:

#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 n;
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL);
    flags |= O_NONBLOCK;
    if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1)
    {
        perror("fcntl");
        exit(1);
    }
    tryagain:
        n = read(STDIN_FILENO, buf, 10);
        if (n < 0) {
            if (errno == EAGAIN) {
                sleep(1);
                write(STDOUT_FILENO, MSG_TRY,
                strlen(MSG_TRY));
                goto tryagain;
            }
        perror("read stdin");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
}                    

六、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;
}

七、mmap

  mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要read/write函数。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
int munmp(void *addr, size_t len);

  该函数各参数的作用图示如下:

  如果addr参数为NULL,内核会自己在内存地址空间中选择合适的地址建立映射。如果addr不是NULL,内给内核一个提示应该从什么地址开始映射,内核会选择addr之上的某个合适的地址开始映射。建立映射后,真正的映射首地址通过返回值可以得到。len参数是需要映射的那一部分文件的长度。off参数是从文件的什么位置开始映射,必须是页大小的整数倍(在32位体系统结构上通常是4K)。filedes是代表该文件的描述符。

  prot参数有四种取值:

  • PROT_EXEC 表示映射的这一段可执行,例如映射共享库
  • PROT_READ 表示映射的这一段可读
  • PROT_WRITE 表示映射的这一段可写
  • PROT_NONE 表示映射的这一段不可访问

  flag参数有很多种取值,以下是其中两种:

  • MAP_SHARED 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。
  • MAP_PRIVATE 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。

  如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。当进程终止时,该进程的映射内存会自动解除,也可以调用munmap解除映射。munmap成功返回0,出错返回-1。

  mmap函数的底层也是一个函数调用,在执行程序时经常要用到这个系统调用来映射共享库到改进程的地址空间。

时间: 2024-10-25 17:00:17

C标准I/O库函数与Unbuffered I/O函数的相关文章

标准c库函数与Linux下系统函数库 区别 (即带不带缓冲区的学习)

我们都知道,C语言在UNIX/Linux系统下有一套系统调用(系统函数),比如文件操作open().close().write().read()等,而标准C语言的库函数中也有一套对文件的操作函数fopen().fclose().fwrite().fread()等..那么同样是对文件的操作函数,标C与UC有什么区别呢?是标C效率高还是UC效率高呢?今天就让我们来一探究竟. 程序作用:将0~999999这1000000个整型数据写入文件. 1.标准C实现大量数据写入文件: /*文件名test1.c*

C标准库-数值字符串转换与内存分配函数

原文链接:http://www.orlion.ga/977/ 一.数值字符串转换函数 #include <stdlib.h> int atoi(const char *nptr); double atof(const char *nptr); 返回值:转换结果 atoi把一个字符串开头可以识别成十进制整数的部分转换成int型,例如atoi("   -123abc")返回-123(字符串开头可以有空格).如果字符串开头没有可识别的整数返回0,而atoi("0abc&

标准IO: 文件的打开与关闭函数 fopen &amp; fclose

(1) 流(stream)和文件(file)    流和文件 在Turbo C2.0中是有区别的, Turbo C2.0 为编程者和被访问的设备之间提供了一层抽象的东西, 称之为"流", 而将具体的实际设备叫做文件.流是一个逻辑设备, 具有相同的行为.因此, 用来进行磁盘文件写的函数也同样可以用来进行打印机的写入.在Turbo C2.0中有两种性质的流:   文字流( text stream)和二进制(binary stream).对磁盘来说就是文本文件和二进制文件.本软件为了便于让读

[C/C++标准库]_[初级]_[map的查找函数分析]

场景: 1. map在查找非数值索引(数值非重复索引可以使用vector)的对象时是高效率的,因为用的红黑树的实现,查找和插入都是logarithmic time 效率很高. 2.map可以说是很实用的数据结构. 3.使用multimap可以存储重复key,使用场景就是1对多的情况,比如一个联系人对应多个分组. void TestMap() { //map一般是通过红黑树来实现.http://en.cppreference.com/w/cpp/container/map //multimap也一

C++标准库(七)之图解bind函数对象

bind与普通函数的绑定 1. bind可以将用户提供的需要一个参数的函数转换为一个不需要参数的函数对象: 2.使用非成员函数,在使用前和使用时提供参数: bind与类的非静态成员函数的绑定 1.非静态函数成员的使用需要一个隐式的this参数 2.也可以将一个隐式的函数指针显式的传递给需要一个参数的函数对象 3.函数对象经常同时使用之前绑定参数和调用时提供参数: 参考: 1.http://blog.think-async.com/2010/04/bind-illustrated.html 2.<

task_struct、files_struct

每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符(Process Descriptor),而在操作系统理论中称为进程控制块(PCB,Process Control Block).task_struct中有一个指针指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针,如下图所示. files_struct位于内核,用户程序不能直接访.但是可以使用files_struct的索引(即0.1.2.3这些数字),

APUE之通过popen,fputc等函数获取本虚拟机网卡eth0的IP

任务:unix环境通过c程序获取本虚拟机网卡eth0的IP. 总结: 1. 标准I/O库函数相对于系统调用的函数多了个缓冲区(,buf),安全性上通过buf 防溢出. 2.用system函数输出是标准输出,进一步理解fork函数和exec函数重新开启一个进程运行程序: 3.printf 这类输出函数中" "若包含"记得要换成转义字符\"            资料链接:   http://blog.csdn.net/ce123_zhouwei/article/det

UNIX环境高级编程---标准I/O库

前言:我想大家学习C语言接触过的第一个函数应该是printf,但是我们真正理解它了吗?最近看Linux以及网络编程这块,我觉得I/O这块很难理解.以前从来没认识到Unix I/O和C标准库I/O函数压根不是一码事.Unix I/O也叫低级I/O,也叫Unbuffered I/O,是操作系统内核部分,也是系统调用:而C标准I/O函数相对也成Buffered I/O,高级I/O,一般是为了效率考虑对这些系统调用的封装.以前使用getchar()经常为输入完后的回车而出错.那是不理解标准I/O实现时的

无缓存I/O操作和标准I/O文件操作区别

本文转载于:http://www.360doc.com/content/11/0521/11/5455634_118306098.shtml 首先,先稍微了解系统调用的概念:       系统调用,英文名systemcall,每个操作系统都在内核里有一些内建的函数库,这些函数可以用来完成一些系统系统调用把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序,如果没有系统调用和内核函数,用户将不能编写大型应用程序,及别的功能,这些函数集合起来就叫做程序接口或应用编程