文件I/O

Posted rainbow1122

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了文件I/O相关的知识,希望对你有一定的参考价值。

文件I/O

1.C标准函数与系统函数的区别1.c标准函数和系统函数的区别

  接下来用应用层API的知识。每当打开一个文件,默认打开标准输入,标准输出,标准出错流三个流,每个FILE都对应一个缓冲区,默认大小为8192Byte。

 

 2.PCB概念(进程控制块)

(1)task_struck结构体

  可以自己动手查看  vi  /usr/src/linux-headers/include\\linux\\sched.h

  在linux中,把每一个进程的基本信息抽象成一个结构体,这就是task_struct结构体,在include\\linux\\sched.h文件中定义。每个进程都会被分配一个task_struct结构,它包含了这个进程的所有信息。在任何时候,操作系统都能跟踪这个结构的信息。

struct task_struct {
volatile long state;
/ /这个是进程的运行时状态,-1代表不可运行,0代表可运行,>0代表已停止。
可运行状态 TASK_RUNNING
处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current总是指向运行队列中的某个元素,只是具体指向谁由调度程序决定。

等待状态 TASK_INTERRUPTIBLE可中断 TASK_UNINTERRUPTIBLE不可中断 
处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中。Linux中处于等待状态的进程分为两种:可中断的等待状态和不可中断的等待状态。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。

暂停状态TASK_STOPPED
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。

僵死状态TASK_ZOMBIE
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。

(2)files struck 每个PCB里边有一个,本质是一个数组。

(3)open/close

  转自:http://joe.is-programmer.com/posts/17463.html

  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参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可以是绝对路径。

flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or

必选项:以下三个常数中必须指定一个,且仅允许指定一个。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写打开

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


第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r--r--,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。注意:创建文件权限不能大于执行程序用户的自有权限,如O_CREAT选项时,忘加mode参数,则新创建的文件从栈帧中取出垃圾值为mode。
close函数关闭一个已打开的文件:

#include <unistd.h>
int close(int fd);

  返回值:成功返回0,出错返回-1并设置errno

  参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。


给个例子:

复制代码
//filename:code
//
#include <error.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<stdio.h>
#include<unistd.h>
int main(void) { int fd;
char buf[1024]="helloworld";
fd=open("abc",O_CREAT|O_RDWR,0777);
write(fd,buf,strlen(buf));
printf("fd=%d\\n",fd);
close(fd);
return 0; }

执行:gcc open.c -o app 查看结果:$umask 0002 $ls -l abc -rwxrwxr–x 1 joseph joseph 0 11月7日 10:24 abc 以上例子以mode 0777创建一个文件mytest,而umask是0002, 所以创建的文件权限是0777-0002=0775(-rwxrwxr–x)。

 

最大打开文件个数

一般默认为1024,查看方式  

ulimit -a

其中的open files 就是,改变方式 ulimit -n 文件数,最多能打开个个数,查看方式  cat /proc/sys/fs/file-max,数目与内存的大小有关

(4)read/write

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

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0

 

参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:

1、读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。

2、从终端设备读,通常以行为单位,读到换行符就返回了。

3、从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数。

 

write函数向打开的设备或文件中写数据。
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

返回值:成功返回写入的字节数,出错返回-1并设置errno

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

 

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

实例:

 1 #include<unistd.h>
 2 #include<sys/stat.h>
 3 #include<sys/types.h>
 4 #include<fcntl.h>
 5 #include<stdlib.h>
 6 #include<strio.h>
 7 
 8 #define SIZE 8192
 9 
10 int main(int argc,char *argv[])
11 {
12        char buf[SIZE];
13        int  fd_src,fd_dest,len;
14        if(argc<3)
15        {
16               printf("./mycp src dest\\n");
17               exit(1);
18        }
19         fd_src=open(argv[1],O_RDONLY);
20           fd_dest=open(argv[2],O_CREAT|O_WRONLY|O_TRUNC,00644);
21 
22        while((len=read(fd_src,buf,sizeof(buf))>0)
23        {
24                    write(fd_dest,buf,len);
25         }
26         close(fd_src);
27         close(fd_dest);
28         return 0;
29 }

(5)阻塞和非阻塞(阻塞相当于c++中的cin,一直等待键盘输入)

转自http://blog.csdn.net/u012317833/article/details/39343915

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

  • 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。

  • 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

下面这个小程序从终端读数据再写回终端。

阻塞读终端
  1. #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;  
    }  

     

执行结果如下:
  1. [root@localhost apue2]# ./a.out  
    hello  
    hello  
    [root@localhost apue2]# ./a.out   
    hello world  
    hello worl[root@localhost apue2]# d  
    bash: d: command not found  
    [root@localhost apue2]#   

  第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:

  1. Shell进程创建a.out进程,a.out进程开始执行,而Shell进程睡眠等待a.out进程退出。

  2. a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。

  3. a.out进程打印并退出,这时Shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执行,结果发现执行不了,没有d这个命令。

  如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errnoEWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

 
  1. while(1) {  
        非阻塞read(设备1);  
        if(设备1有数据到达)  
            处理数据;  
        非阻塞read(设备2);  
        if(设备2有数据到达)  
            处理数据;  
        ...  
    }  

     

  如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。

非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行

 
  1. while(1) {  
        非阻塞read(设备1);  
        if(设备1有数据到达)  
            处理数据;  
        非阻塞read(设备2);  
        if(设备2有数据到达)  
            处理数据;  
        ...  
        sleep(n);  //多出来的部分
    }  

     

  这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。

  以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终端,但是没有O_NONBLOCK标志。读标准输入是阻塞的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK标志。

非阻塞读终端
 
  1. #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) { //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; }

  以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。

非阻塞读终端和等待超时

 
    1. #include <unistd.h>  
      #include <fcntl.h>  
      #include <errno.h>  
      #include <string.h>  
      #include <stdlib.h>  
        
      #define MSG_TRY "try again\\n"  
      #define MSG_TIMEOUT "timeout\\n"  
        
      int main(void)  
      {  
          char buf[10];  
          int fd, n, i;  
          fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);  
          if(fd<0) {  
              perror("open /dev/tty");  
              exit(1);  
          }  
          for(i=0; i<5; i++) {  
              n = read(fd, buf, 10);  
              if(n>=0)  
                  break;  
              if(errno!=EAGAIN) {  
                  perror("read /dev/tty");  
                  exit(1);  
              }  
              sleep(1);  
              write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));  
          }  
          if(i==5)  
              write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));  
          else  
              write(STDOUT_FILENO, buf, n);  
          close(fd);  
          return 0;  
      }  
      注:error和perror

       

    2. perror会根据errno返回的序号,查找对应的出差信息并打印

以上是关于文件I/O的主要内容,如果未能解决你的问题,请参考以下文章

java I/O流基础(知识+代码示例)

对文件 I/O,标准 I/O 的缓冲的理解

监控文件描述符的六种方式(进程监控selectpoll非阻塞轮询I/O异步I/O线程监控)

asyncio 是不是支持文件操作的异步 I/O?

Scala 文件 I/O

Linux I/O重定向