admin管理员组

文章数量:1558103

实查man手册
(1)当我们写应用程序时,很多API原型都不可能记得,所以要实时查询,用man手册
(2)man 1 xx查linux shell命令,man 2 xxx查Linux系统调用, man 3 xxx查库函数(API)

 一、文件与文件类型


1、文件定义


    定义:文件(File)是一个具有符号名字的一组相关联元素的有序序列。文件可以包含的内容十分广泛,操作系统和用户都可以将具有一定独立功能的一个程序模块、一组数据或一组文字命名为一个文件。

    文件名:这个数据有序序列集合(文件)的名称。

2、文件的分类


    文件由许多种,运行的方式也各有不同。在Windows中,我们是通过文件的后缀名来对文件分类的。例如.txt、.doc、.exe等。而在Linux系统中则不是,它不以文件后缀来区分文件的类型。

在Linux中,我们可以使用ls -l指令来查看文件的类型。在Linux系统中,文件主要有7种类型。

 1.    -    普通文件    指ASCII文本文件、二进制文件以及硬链接文件

1)文本文件。文件中的内容是由文本构成的,文本指的是ASCII码字符。文件里的内容本质上都是数字(不管什么文件内容本质上都是数字,因为计算机中本身就只有1和0),而文本文件中的数字本身应该被理解为这个数字对应的ASCII码。常见的.c文件, .h文件  .txt文件等都是文本文件。文本文件的好处就是可以被人轻松读懂和编写。所以说文本文件天生就是为人类发明的。
(2)二进制文件。二进制文件中存储的本质上也是数字,只不过这些数字并不是文字的编码数字,而是就是真正的数字。常见的可执行程序文件(gcc编译生成的a.out,arm-linux-gcc编译连接生成的.bin)都是二进制文件。
(3)对比:从本质上来看(就是刨除文件属性和内容的理解)文本文件和二进制文件并没有任何区别。都是一个文件里面存放了数字。区别是理解方式不同,如果把这些数字就当作数字处理则就是二进制文件,如果把这些数字按照某种编码格式去解码成文本字符,则就是文本文件。
(4)我们如何知道一个文件是文件文件还是二进制文件?在linux系统层面是不区分这两个的(譬如之前学过的open、read、write等方法操作文件文件和二进制文件时一点区别都没有),所以我们无法从文件本身准确知道文件属于哪种,我们只能本来就知道这个文件的类型然后用这种类型的用法去用他。有时候会用一些后缀名来人为的标记文件的类型。
(5)使用文本文件时,常规用法就是用文本文件编辑器去打开它、编辑它。常见的文本文件编辑器如vim、gedit、notepad++、SourceInsight等,我们用这些文本文件编辑器去打开文件的时候,编辑器会read读出文件二进制数字内容,然后按照编码格式去解码将其还原成文字展现给我们。如果用文本文件编辑器去打开一个二进制文件会如何?这时候编辑器就以为这个二进制文件还是文本文件然后试图去将其解码成文字,但是解码过程很多数字并不对应有意义的文字所以成了乱码。
(6)反过来用二进制阅读工具去读取文本文件会怎么样?得出的就是文本文字所对应的二进制的编码。
 

2.    d    目录文件    包含若干文件或子目录

(1)目录就是文件夹,文件夹在linux中也是一种文件,不过是特殊文件。用vi打开一个文件夹就能看到,文件夹其实也是一种特殊文件,里面存的内容包括这个文件的路径,还有文件夹里面的文件列表。
(2)但是文件夹这种文件比较特殊,本身并不适合用普通的方式来读写。linux中是使用特殊的一些API来专门读写文件夹的。
 

3.    l    符号链接    只保留所指向文件的地址而非文件本身

4.    p    管道文件    用于进程间通信

5.    c    字符设备    原始的I/O设备文件,每次操作仅操作1个字符(例如键盘)

6.    b    块设备        按块I/O设备文件(例如硬盘)

(1)设备文件对应的是硬件设备,也就是说这个文件虽然在文件系统中存在,但是并不是真正存在于硬盘上的一个文件,而是文件系统虚拟制造出来的(叫虚拟文件系统,如/dev /sys /proc等)
(2)虚拟文件系统中的文件大多数不能或者说不用直接读写的,而是用一些特殊的API产生或者使用的

7.    s    套接字        套接字是方便进程间通信的特殊文件,与管道不同的是套接字能通过网络连接使不同的计算机的进程进行通信

3、Linux的文件目录结构


    Linux系统中文件采取树形结构,即一个根目录(/),包含下级目录或文件的信息;子目录又包含更下级的目录或文件的信息。依次类推层层延伸,最终构成一棵树。

Linux系统的每个目录都有其特定的功能,这里只简单介绍一些主要目录及其功能

目录            功能说明

/etc            存放系统配置文件

/bin            存放常用指令

/sbin        (root用户的)存放指令目录

/home        用户主目录,所有用户p的文件默认建立在此目录下(用户工作目录)

/boot          包含内核启动文件

/dev            存放设备文件(与底层驱动交互)

/usr             存放应用程序

/mnt            挂载目录

/root            root用户主目录

/proc            process的所写,存放描述系统进程的详细信息

/lib               存放常见库文件

/lost+found    可以找到一些误删除或丢失的文件并恢复它们
 


 关于文件必须知道的事

(1)文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件。当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
(2)打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。


常见的一些现象:
第一个:打开一个大文件时比较慢
第二个:我们写了一半的文件,如果没有点保存直接关机/断电,重启后文件内容丢失。

这就是因为上述的(1)(2)点的原因


为什么要这么设计 而不是直接在块设备上直接读写?
以为块设备本身有读写限制(回忆NnadFlash、SD等块设备的读写特征),本身对块设备进行操作非常不灵活。而内存可以按字节为单位来操作,而且可以随机操作(内存就叫RAM,random),很灵活。所以内核设计文件操作时就这么设计了。


二.linux系统如何管理文件?

也就是说对于每一个进程的进程控制块(PCB,在内核中使用数据结构task_struct来表示)中都会有一个文件描述符表(这是个数组) 用来表示这个进程打开的文件。

其中对于标准输入、标准输出、标准错误 是在每个进程的文件描述符表中的下标0、1、2的位置.所以我们相要访问标准输入、标准输出、标准错误 就可以直接使用文件描述符0、1、2来访问

UNIX环境下的文件共享

什么是文件描述符?
(1)文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分。
(2)一句话讲清楚文件描述符:文件描述符就是用来区分一个程序打开的多个文件的。
(3)文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了

文件描述符用来表征一个文件,但是为什么操作系统要用这么一个整数来表征一个文件呢?这就操作系统底层实现有莫大的关系。
  在进程PCB(进程控制块)中有着这么一个部分,IO状态信息,说的再具体点,在PCB中存在着一张表,我们可以叫它文件描述符表(。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是下标(index),文件表指针是value),这张表每个进程都会有 且为进程独有,所以它是进程级的。这张表上的每一个表项都有两个部分组成,文件描述符标志以及一个文件指针(注意这个文件指针不是FILE* 在标准IO有讲)。其中文件描述符标志也就是我们所使用的文件描述符 -- fd,当然我们也可以将其看做是这张表的下标。这张表长这样。

文件描述符表中每一项都有一个文件指针,那么这个指针又指向哪里呢?这就要提到另一张表 打开文件描述表(open file description table),简称打开文件表。,注意这张表由操作系统管理,且系统中只有唯一一张这样的表,但它有许多的表项(类似于只有一个数组但有许多个元素),因此这张表是系统级的。这张表中的每一项都存储着一个文件与一个进程相关的一些信息,其中主要分为三个部分:文件状态标志,文件当前偏移量,v-node结点指针。
  文件状态标志就是文件在打开时的状态标志,例如可读,可写,可读写,阻塞等都会记录在其中,这些状态标志也可以使用fcntl函数修改。

当进程打开一个文件时,内核就会创建一个新的 打开文件表表项。需要注意的是, 打开文件表表项不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的 打开文件表表项,从而共享这个打开的文件。
  文件当前偏移量就是文件指针当前在文件中指向的位置,我们可以用lseek函数修改。
  v-node结点指针我们稍后再谈,现在我们要详细讲讲这张表的工作过程。打开文件表是属于系统级的,系统中任何进程打开任何文件都会在其中添加一个记录项,按照一般情况下来说两个不同的进程打开相同的文件也会在表中创建两个不同的表项,因此两个进程对同一个文件可以有不同的状态标志以及文件当前偏移量,一个进程中不同的文件描述符所代表的文件描述符表项中的文件指针也该指向不同的打开文件表项,但是在某些情况下文件描述符表中不同表项的指针却又有可能指向系统级打开文件表中的同一个表项。例如我们在fork子进程时,子进程复制父进程PCB中的大部分信息包括IO状态信息时会复制文件描述符表,因此两个不同的进程此时就会打开同一个文件,并且文件指针的指向也不会改变会指向相同的打开文件表表项;在使用dup函数重定向时一个进程中不同文件描述符表项中的文件指针也会指向同一个打开文件表中的表项。
  这张表中的每个表项长这样。

 最后还剩一个问题,这个v-node节点指针干嘛用的?v-node节点指针当然指向v-node节点的啊。那么什么是v-node节点?说到v-node就不得不提起i-node节点,在UNIX操作系统中操作系统管理文件的方式是通过使用v-nodei-node节点的方式进行管理的,每个文件都会有这样的节点用于保存相关的文件信息,例如v-node节点上保存了文件类型,对这个文件进行操作的函数指针以及对应的i-node节点的指针;而i-node节点上保存了文件长度,文件数据存储在磁盘的位置,文件所属者等。这些文件信息平时存储在磁盘上,当一个文件倍打开时系统会将这些信息读入内存,并且相同的文件的i-nodev-node节点在内存中只会存在一份。这两个节点长这样。

 那么为什么要用两个节点保存这些信息呢?这是为了在一个操作系统上对多文件系统进行支持。把与文件系统无关的文件信息存储在v-node节点上,其余信息存在i-node上,分开存储,这样的系统也叫做虚拟文件系统
  综上所述,把以上集中数据结构连接起来就构成了一个进程对文件进行控制的完整脉络,进程也就得到了和文件控制有关的所有信息,可见并不是所有文件信息都保存在PCB中的。

 对于两个不同的进程打开同一个文件,他们的文件指针可能指向不同的打开文件表项,但是最终都会指向同一个v-node和i-node节点,正如之前所说,相同文件的有关信息在内存中只会存在一份。如下图。


*Linux环境下的文件共享

linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是下标(index),文件表指针是value

而Linux比较特殊,他其中没有v-node节点而是用了两个不同的i-node节点,但是结果而言大同小异。传统的Unix既有v节点(vnode)也有i节点(inode),vnode的数据结构中包含了inode信息。但在Linux中没有使用vnode,而使用了通用inode。“实现虽不同,但在概念上是一样的。”
vnode (“virtual node”)仅在文件打开的时候,才出现的;而inode定位文件在磁盘的位置,它的信息本身是存储在磁盘等上的,当打开文件的时候从磁盘上读入内存。
 

linux系统编程之基础必备(三):文件描述符file descriptor与inode的相关知识_Super的博客-CSDN博客每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的 信息,称为进程描述符(Process Descriptor),而在操作系统理论中称为进程控制块 (PCB,Process Control Block)。task_struct中有一个指针(struct files_struct *files; )指向files_struct结构体,称为文件 描述符表,其中每个表项包含一个https://blog.csdn/Sandeldeng/article/details/52902740

linux中的打开文件表表项(file结构体)

file结构中主要保存了文件位置,此外,还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。

file结构在include\linux\fs.h中定义如下:

struct file 
 
{
 
 struct list_head        f_list;    /*所有打开的文件形成一个链表*/
 
 struct dentry           *f_dentry; /*指向相关目录项的指针*/
 
 struct vfsmount         *f_vfsmnt; /*指向VFS安装点的指针*/
 
 struct file_operations  *f_op;     /*指向文件操作表的指针*/
 
 mode_t f_mode;                                  /*文件的打开模式*/
 
 loff_t f_pos;                                   /*文件的当前位置*/
 
 unsigned short f_flags;                         /*打开文件时所指定的标志*/
 
 unsigned short f_count;                           /*使用该结构的进程数*/
 
 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
 
 /*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
 
 int f_owner;                  /* 通过信号进行异步I/O数据的传送*/
 
 unsigned int         f_uid, f_gid;  /*用户的UID和GID*/
 
 int                 f_error;       /*网络写操作的错误码*/
 
 
 
 unsigned long f_version;           /*版本号*/
 
 void *private_data;                      /* tty驱动程序所需 */
 
};

内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述表中每一项都是一个指针,指向一个用于描述打开的文件的数据块———file对象,file对象中描述了文件的打开模式,读写位置等重要信息,当进程打开一个文件时,内核就会创建一个新的file对象。需要注意的是,file对象不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的file对象,从而共享这个打开的文件。file对象有引用计数,记录了引用这个对象的文件描述符个数,只有当引用计数为0时,内核才销毁file对象,因此某个进程关闭文件,不影响与之共享同一个file对象的进程.

file对象中包含一个指针,指向dentry对象(目录项)。dentry对象(目录项)代表一个独立的文件路径如果一个文件路径被打开多次,那么会建立多个file对象,但它们都指向同一个dentry对象

dentry对象(目录项)中又包含一个指向inode对象的指针。inode对象代表一个独立文件因为存在硬链接符号链接,因此不同的dentry对象(目录项)可以指向相同的inode对象(这是硬链接原理).inode 对象包含了最终对文件进行操作所需的所有信息,如文件系统类型、文件的操作方法、文件的权限、访问日期等。

打开文件后,进程得到的文件描述符实质上就是文件描述符表的下标,内核根据这个下标值去访问相应的文件对象,从而实现对文件的操作。

文件描述符表中的文件指针 -> file结构(打开文件表)-> dentry对象(代表一个独立的文件路径)-> inode对象(代表一个独立文件

注意,同一个进程多次打开同一个文件时,内核会创建多个file对象。

当进程使用fork系统调用创建一个子进程后,子进程将继承父进程的文件描述符表,因此在父进程中打开的文件可以在子进程中用同一个描述符访问。

看懂这两个图!!!!!!!! 

上图中的i节点数组就是多个inode组成的数组 存放在柱面组中的 i节点

 数据块 包含了许多的小数据块目录块 文件的数据通常存放在 数据块中的小数据块中 一个文件通常对应一个inode 但往往要占用多个小数据块

上图中的 目录块 相当于将一个文件名指向 一个inode 

所以可以通过

多个目录块 将不同文件名指向同一个inode 这样就形成了 硬链接

Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:

1.首先,系统找到这个文件名对应的inode号;

2.其次,索引查找inode表,获取inode信息;

3.最后,根据inode信息,找到文件数据所在的block,读出数据。程序通过文件名获取文件内容的过程如下

(相当于通过文件名找到对应的i-node号 然后找到对应的i-node节点 然后通过对应的i-node节点找到对应的数据块 也就是对应文件的数据)

inode结构体记录了很多关于文件的信息,比如文件长度,文件所在的设备,文件的物理位置,创建、修改和更新时间等等,特别的,它不包含文件名!目录下的所有文件名和目录名都存储在目录的数据块中,即如图的目录块对于常规文件,文件的数据存储在数据块中,一个文件通常占用一个inode,但往往要占用多个数据块,数据块是在分区进行文件系统格式化时所指定的“最小存储单位”,块的大小为扇区的2^n倍,一个扇区512B。

 当我们用ls 查看某个目录或文件时,如果加上-i 参数,就可以看到inode节点了;比如ls -li lsfile.sh ,最前面的数值就是inode信息。

软链接和硬链接 

一个文件从外到内分为:我们可以看到“文件名称”,文件名称对应一个inode,inode对应一个物理存储的文件数据

软链接相当于建立了一个新的快捷方式文件,该文件有自己的名称和inode以及物理存储的文件数据,文件数据里记录着如何跳转的设置数据,访问该快捷文件会被重新定向到原始文件删除原始文件,软链文件失效

硬链接相当于为当前文件名对应的文件再建立了一个文件别名,别名对应的inode以及物理数据都是一样的,一旦建立,我们甚至根本无法区分谁是原始文件的原始名称删除文件的其中一个名称,文件不会丢失,除非把所有的名称都删除

如上图:hard link(硬链) 和file 都指向同一个 inode,inode对应了一个实际物理存储的文件。soft link(软链) 对应一个新的inode, 新的inode对应一个新的物理存储文件,物理存储文件又指向了目标文件 file。

软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。比如:A是B的软链接(A和B都是文件名),A的目录项中的inode节点号与B的目录项中的inode节点号不相同,A和B指向的是两个不同的inode,继而指向两块不同的数据块。但是A的数据块中存放的只是B的路径名(可以根据这个找到B的目录项)。A和B之间是“主从”关系,如果B被删除了,A仍然存在(因为两个是不同的文件),但指向的是一个无效的链接

多个文件共用一个inode,同样可以实现链接?!这就是硬链接的原理inode中有链接计数器,当增加一个文件指向这个inode时,计数器增1(软链接不会让计数器+1)。特别的,当计数器为0时候(inode_operations中的unlink 减一操作),文件才真正从磁盘删除因为硬链接文件和源文件 是在不同的目录块中由不同的文件名对应同一个inode 这就是为什么硬链接文件在源文件删除后 任然会保留着源文件的数据 可以继续使用 

简单的说:

1.软链接 相当于是快捷方式

软链接文件的inode和源文件的inode不一样 指向的数据块也不一样 其中 但是软链接的数据块中存放的只是源文件的路径名(可以根据这个找到源文件的目录项)。软链接文件之间是“主从”关系,如果源文件被删除了,软链接文件仍然存在(因为两个是不同的文件),但指向的是一个无效的链接

2.硬链接 相当于是复制了一份加同步更新

但是这两个文件在目录块中的文件名不一样 但是都指向同一个inode 即硬链接文件和源文件的inode是一样的 指向同样的数据块 inode以及物理数据都是一样的,一旦建立,我们甚至根本无法区分谁是原始文件的原始名称

软连接和硬链接的区别看这个博客

Linux中硬链接与软链接的区别与联系_皮卡丘的博客-CSDN博客链接为 Linux 系统解决了文件的共享使用问题,还带来了隐藏文件路径、增加权限安全及节省存储等好处。链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link),也即软链接(Soft Link)。本文,我们将详细讲解关于这两种链接的区别与联系。https://blog.csdn/u012294618/article/details/72615474



硬盘中的静态文件和inode(i节点)
(1)文件平时都在存放在硬盘中的,硬盘中存储的文件以一种固定的形式存放的,我们叫静态文件。
(2)一块硬盘中可以分为两大区域:一个是硬盘内容管理表项,另一个是真正存储内容的区域。操作系统访问硬盘时是先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息,然后再用这个信息去查询真正存储内容的区域,最后得到我们要的文件。
(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表(我们叫inode,i节点,其实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中就包括文件名、文件在硬盘上对应的扇区号、块号那些东西·····)
强调:硬盘管理的时候是以文件为单位的,每个文件一个inode,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。
(4)联系平时实践,大家格式化硬盘(U盘)时发现有:快速格式化和底层格式化。快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的。

内存中被打开的文件和vnode(v节点)
(1)一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程。每个进程都有一个数据结构用来记录这个进程的所有信息(叫进程信息表),表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode
(2)一个vnode中就记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。

关于文件描述符的注意点:(文件描述符是一个非负整数)

  1. 文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分。
  2. (2)一句话讲清楚文件描述符:文件描述符就是用来区分一个程序打开的多个文件的。
  3. (3)文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义
  4. 文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。
  5. 文件描述符这个数字是open系统调用内部由操作系统自动分配的,操作系统分配这个fd时也不是随意分配,也是遵照一定的规律的,我们现在就要研究这个规律。
  6. 操作系统规定,fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value
  7. 当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。也就是说如果之前fd已经占满了0-9,那么我们下次open得到的一定是10.(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了)
  8. fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。
  9. linux内核占用了0、1、2这三个fd是有用的,当我们运行一个程序得到一个进程时,内部就默认已经打开了3个文件,这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入、标准输出、标准错误。
  10. 标准输入一般对应的是键盘(可以理解为:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件)
  11. printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fpirntf,这个函数就可以指定输出到哪个文件描述符中。

三.系统调用与用户编程接口(API)


    系统调用System Call)是由操作系统实现提供的所有系统调用所构成的程序接口的集合。是应用程序与操作系统间的接口与纽带。

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有良好的兼容性。为了达到这个目的,内核提供一系列具备预定功能的函数,通过系统调用的接口呈现给用户。当用户访问系统调用,系统调用把应用程序的请求传递给内核,调用相应的内核函数完成所需处理,将处理结果返回给应用程序。

    应用程序编程接口(API,Application Programming Interface)是一些预定义的函数,目的是提供应用程序与开发人员基于软件/硬件得以访问一组例程的能力,而又无需访问源码或理解内部工作原理机制。

在实际开发应用程序的过程中,我们并不直接使用系统调用接口,而是使用用户编程接口(API)。为什么呢?

        1.系统调用功能非常简单,有时无法满足程序的需求。

        2.不同操作系统的系统调用接口不兼容,若使用系统调用接口则程序移植工作量非常大

    用户编程接口使用各种库(在C语言中最主要的是C库)中的函数。为了提高编程效率,C库中实现了很多函数。这些函数实现了许多常用功能供程序开发者调用。这样一来,程序开发者无需自己编写这些代码,直接可以调用函数就能实现功能,提高了编码效率和代码复用率。

    使用用户编程接口还有一个好处:一定程度上解决了程序的可移植性(虽然C语言的可移植性仍没有Java好)。几乎所有的操作系统上都实现了C库,因此使用C语言编写的程序只需在不同的系统下重新编译即可运行。

    通常情况下,用户编程接口API在实现时需要依赖系统调用接口。例如创建进程API函数fork()需要调用内核空间的sys_fork()系统调用。但是还有一些API无需调用任何系统调用。

比如 

文件IO函数就是 系统调用

标准IO函数就是 API函数

在Linux中用户编程接口遵循在Unix系统中最流行的应用编程编程标准POSIX标准。

/***************POSIX简介*************************/

    POSIX表示可移植操作系统接口(Portable Operating System Interface ,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。

    POSIX标准意在期望获得源代码级别的软件可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。

/***************POSIX简介end**********************/

C库fopen缓冲区在用户态完成的 然后写内核缓冲。open 缓冲在内核态用户态没有缓冲


标准I/O与文件I/O的区别:


    1.文件I/O又称为低级磁盘I/O,遵循POSIX标准。任何兼容POSIX标准的操作系统都支持文件I/O。标准I/O又称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境有标准C库,标准I/O就可以使用。

在Linux系统中使用GLIBC标准,它是标准C库的超集,既支持ANSI C中定义的函数又支持POSIX中定义的函数。因此Linux下既可以使用标准I/O,也可以使用文件I/O。

    2.通过文件I/O读写文件时,每次操作都会执行相关系统调用这样的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。标准I/O在文件I/O的基础上封装了缓冲机制,每次先操作缓冲区,必要时再访问文件从而减少了系统调用的次数

    3.文件I/O使用文件描述符打开操作一个文件,可以访问不同类型的文件(例如普通文件、设备文件和管道文件等)。而标准I/O使用FILE指针来表示一个打开的文件,通常只能访问普通文件


什么是当前文件偏移量?

每当打开一个文件都有一个"当前文件偏移量"(current file offset). 它通常是非负整数,用以度量从文件开始处计算的字节数.

注意区别当前文件偏移量和文件偏移量!!!!

当前文件偏移量就是此时此刻你正在操作的文件的文件偏移量 可以通过系统调用函数lseek来修改

这里用系统调用函数open,close,write,read,lseek为例 

重新用open函数打开一个文件 文件偏移量会被重新设置为0,除非指定O_APPEND选项 让当前文件偏移量为文件数据末尾
 

通常,读,写操作都是从当前文件偏移量开始,并使偏移量增加所读写的字节数.按照系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该文件偏移量被设置为0.
就相当于我们往文件里写了多少个字节的数据 当前文件偏移量就是原来的文件偏移量加上写入数据的字节数 即当前文件偏移量就在写入数据的末尾

使用open函数打开文件时

1.若指定O_APPEND选项         则原来文件偏移量就是该文件原有数据字节数

2.若未指定 O_APPEND选项     则原来文件偏移量为0

当我们打开一个空文件时,默认情况偏移量为0所以这时候去write时写入就是从文件开头开始的。write和read函数本身自带增加偏移量的功能,所以当我write了n个字节后,偏移量会自动加n

此后再读写该文件就会从当前文件偏移量开始 
read和write函数都是从当前文件偏移量开始操作的,所以当我们用lseek显式的将文件偏移量改变后,那么再去read/write时就是从改变后的文件偏移量处开始的。

情况1:我们open打开一个空文件write写数据后read读数据

这就是为啥我们直接先write写了12字节,然后直接read 时 将会read是空的(但是此时我们打开文件后发现12字节确实写进来了)因为打开空文件默认文件偏移量为0 写入了12字节 当前文件偏移量就是0+12=12了 再read就是从文件偏移量12处开始 所以读到空数据。

情况2:我们 使用open指定O_APPEND选项 打开一个非空文件有数据x字节) 向其write写数据后 read读数据

因为按照系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该文件偏移量被设置为0.

 所以在write写数据时当前文件偏移量是0 即写入的数据会从头开始写入该文件 即会覆盖原本文件的数据 然后我们使用write写入了n字节的数据 此时该文件的当前文件偏移量大小就是写入的数据字节大小n 加上原本的文件偏移量 0 此时再直接用read读数据就会从当前文件偏移量 n 开始读取数据

情况3:我们 使用open指定了O_APPEND选项 打开一个非空文件(有数据x字节)  向其write写数据后 read读数据

因为指定了 O_APPEND选项 所以该文件的文件偏移量为这个非空文件的数据字节数x(即在文件数据末尾)所以使用write写入了n字节数据后 该文件的当前文件偏移量为 x+n 此时再使用read函数就要从文件偏移量x+n处开始读取数据


四. 流和文件的区别

在初学C语言文件I/O相关知识点时,经常会陷入“什么是流?”“什么是文件?”“流和文件有什么关系(区别)?”等问题。在这里对“流”与“文件”进行简单的讨论

《C Primer Plus》上说,C程序处理一个流而不是直接处理文件。但是后面的解释十分抽象:『流(stream)是一个理想化的数据流,实际输入或输出映射到这个数据流』。

    本质上来说,文件本身就是数据的有序序列,因此我们操作文件时是按顺序依次操作该文件的数据。我们可以想象一个传送带,传送带上的产品就是待操作数据。当我们对文件内的数据进行操作时,已操作的数据从当前位置离开,待操作的数据不断流向当前位置,这样文件内的数据就产生了流动的感觉,这个“传送带”就是C语言内“流”的原型。

    我们打开一个流,就意味着将该流与该文件进行了连接(即让文件内的“产品”放上“传送带”),关闭流将断开流与该文件的连接。此时我们对流进行操作就是对文件进行操作。

(1)流(stream)对应自然界的水流。文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑儿的读写,那么一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。
(2)流这个概念是动态的,不是静态的。
(3)编程中提到流这个概念,一般都是IO相关的。所以经常叫IO流。文件操作时就构成了一个IO流。

流是一种现象,而不是一个物体


1、流是一个抽象的概念,是对信息的一种表达;在程序中,流就是对某个对象输入输出信息的抽象。就像运输工具是对一切运动载体的抽象一样。

2、流是一种“动”的概念静止存储在介质上的信息只有当它按一定的序列准备“运动”时才称为流。“从程序移进或移出字节”就是“动”的表现。静止的信息具有流的潜力,但不一定是流,就像没有汽油不能行走的汽车一样,它具有运输工具的潜力,但它还不是运输工具(因为它很有可能被当作房子来用了,我就在大街上看见有精明的商人用火车车厢来做酒吧)()。
3、流有源头也有目的地;程序中各种移动的信息都有其源和目的,记得编程(特别是汇编)时,老是要确定好某个操作的源操作数和目的操作数。借用佛教一言也即是:“万物皆有因果”,这也就像长江一样,西自唐古拉,而东去太平洋。在高速公路上飞跑的汽车,它必有其出发地和目的地。
4、流一定带有某种信息,没有任何内容的流带着自身来表达“空”信息。就像运输工具一样,它不运货的时候就运着自己这一身的零件(包括驾驶员)并把一样东西运到目的地,那就是它自己和一个“跑空车”的信息。流有最小的信息单元就是二进制位,含有最小的信息包就是字节,C标准库提供两种类型的流:二进制流(binary stream)和文本流(text stream)。二进制流是有未经处理的字节构成的序列;文本流是由文本行组成的序列。而在著名的UNIX系统中,文本流和二进制流是相同的(identical)。

5、流有源头也有目的地,那么它必定与源头和目的地相关联。但人们操作流的时候,最关心的还是其目的地,也就是一个定向(orientation)的意思,就像司机运货一样,它首要关心的问题是目的地,而非起点(操作者都知道)。在C语言中,通过打开流来关联流及其目的地,使用的函数是fopen(),该函数返回一个指向文件的指针(FILE *),该指针包含了足够的可以控制流准确地到达目的地的信息。

Linux下的文件编程所涉及的操作方式都是不带缓冲的I/O,因为每次调用相应的函数比如说read、write等对文件进行操作的时候都会调用内核的系统调用,由于每次都要通过内核对文件进行操作,所以操作效率比较低,对于流编程来说,首先对文件所映射的流进行操作,然后分阶段将相应的数据写入文件,极大地提高了相应的操作效率。Linux也提供了很多流操纵库函数,称为标准I/O库,是ISO C的组成部分。

文件的I/O函数都是针对文件描述符进行操作的,比如说当调用open或者其他函数打开一个文件,返回一个文件的描述符fd,然后针对这个fd进行后续的I/O操作,由于需要多次反复调用对应的系统调用,效率低是自然的。

流I/O函数的操作是围绕流(Stream)进行的,当使用流I/O库打开或者创建一个文件时,可以使一个流和一个文件结合,接下来的操作就是对流进行读写、定位等,最后关闭即可。


上图可以看出流和文件,带缓冲和不带缓冲是相对而言的。对于不带缓冲的文件I/O操作也不是直接对文件进行的,只是在用户空间没有缓冲区,所以是不带缓冲的I/O,但是对于Linux内核来说,还是进行了缓冲。当用户调用不带缓冲的IO函数写数据到文件时,即对磁盘存储区进行读写,Linux内核会先将数据写入到内核中的缓冲存储区。比如说,缓冲存储区的长度是50字节,调用write函数进行写操作时,如果每次写入10个字节,则需要调用5次write函数,而此时数据还是在内核的缓冲区中的,并没有写入到磁盘。当50个字节已经写满的时候才进行实际的IO操作,把数据写入到磁盘中。比如C库fopen缓冲区在用户态完成的 然后写内核缓冲。open 缓冲在内核态用户态没有缓冲

带缓冲的IO则是在用户空间中建立了另外一个缓冲区,即流缓冲区,假设流缓冲区的长度也是50个字节,当调用对应的写入库函数时会将数据写入到这个流缓存里面,然后再一次性进入内核缓存区,此时再使用系统调用将数据写入到文件,从而减少了系统调用

总之:

1.对于不带缓冲的I/O,将数据写入磁盘的过程是:数据→内核缓冲区→磁盘

2.而对于带缓冲的I/O其过程是:数据→流缓冲区→内核缓冲区→磁盘

如果是读取则反向

流操作函数对象不是文件描述符,而是一个流缓冲区(就是在使用标准IO是我们常说的缓冲区)。当打开一个流时,返回一个指向FILE对象的指针。该对象是一个结构体,包含了管理这个流所需要的所有信息,比如说用于实际I/O的文件描述符、指向流缓存的指针、缓存的长度、当前缓存中的字符数、出错标志等。在实际的应用中,用户只需要知道为了引用一个流,需要将FILE指针作为参数传递给对应的函数即可。用户可以简单地把流看做一块由操作系统分配的内存缓冲区,在该缓冲区中存放了文件对应的数据。


五.Linux文件I/O概述


1、POSIX规范


    POSIX(Portable Operating System Interface,可移植操作系统接口规范)标准最初由IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师协会,是目前最大的全球性非营利性专业技术学会)制定,目的是提高UNIX环境下程序的可移植性。通俗来讲,为一个兼容POSIX标准的操作系统编写的应用程序,可以在任何其他兼容POSIX标准的操作系统上编译执行而无需修改代码。常见的Linux与UNIX系统都支持POSIX标准。

2、虚拟文件系统VFS


    Linux系统的一个成功的关键因素是它具有与其他操作系统共存的能力。Linux的文件系统由两层结构搭建:上面的虚拟文件系统VFS(Virtual File System),和下面的各种不同的具体文件系统(例如Ext、FAT32、NFS等)。

VFS将各种具体的文件系统的公共部分抽取出来形成一个抽象层,位于用户的程序与具体需要使用的系统中间,并提供系统调用接口。这样我们只需针对VFS提供的系统调用进行文件操作而无需具体考虑底层细节。VFS屏蔽了用户对底层细节的描述使得编程简化。

可以使用指令:

cat /proc/filesystems

查看当前操作系统支持哪些具体文件系统。

3、文件与文件描述符

Linux操作系统是基于文件概念搭建起来的操作系统(“万物皆文件”),基于这一点,所有的I/O设备都可以直接当做文件来处理。因此操作普通文件的操作函数与操作设备文件的操作函数是相同的,这样大大简化了系统对不同设备、不同文件的处理,提高了效率。

那么对于内核而言,内核是如何区分不同的文件呢?内核使用文件描述符来索引打开的文件。文件描述符是一个非负整数,每当打开一个存在的文件或创建一个新文件的时候,内核会向进程返回一个文件描述符,当对文件进行相应操作的时候,使用文件描述符作为参数传递给相应的函数。

通常一个进程启动时,都会打开三个流:标准输入、标准输出、标准错误输出,这三个流的文件描述符分别是0、1、2,对应的宏定义是STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。可以查看头文件unistd.h查看相关定义。

流的名称        文件描述符              宏定义

标准输入               0                   STDIN_FILENO

标准输出               1                   STDOUT_FILENO

标准错误输出        2                   STDERR_FILENO

基于文件描述符的I/O操作虽然不能直接移植到诸如Windows系统等之外的操作系统上,但对于某些底层的I/O操作(例如驱动程序、网络连接等)是唯一的操作途径。


 文件读写的一些细节

errno和perror
(1)errno就是error number,意思就是错误号码。linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。
(2)errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。
(3)errno本身实质是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字(譬如-37),不适应于人看。
(4)linux系统提供了一个函数perror(意思print error),perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来。
(5)如果我们使用的库函数或者系统调用 在返回错误时会设置 errno 我们就可以使用perror来打印错误信息

read和write的count
(1)count和返回值的关系。count参数表示我们想要写或者读的字节数,返回值表示实际完成的要写或者读的字节数。实现的有可能等于想要读写的,也有可能小于(说明没完成任务)
(2)count再和阻塞非阻塞结合起来,就会更加复杂。如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞住,等待剩余的10个可以读。
(3)有时候我们写正式程序时,我们要读取或者写入的是一个很庞大的文件(譬如文件有2MB),我们不可能把count设置为2*1024*1024,而应该去把count设置为一个合适的数字(譬如2048、4096),然后通过多次读取来实现全部读完。
 


六.文件I/O编程

/*************************************************************************************/

我们知道对于每个进程都会有一个进程控制块(PCB,在内核中使用数据结构task_struct)来描述,在每个进程的PCB中都会有一个文件描述符表(这是个数组) 用来表示这个进程打开的文件。

其中文件描述符表的第0、1、2个元素 也就是值为0、1、2的文件描述符是固定的 分别代表为:标准输入、标准输出、标准错误。也就是说 Linux中一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这三个文件分别对应文件描述符0、1、2。

所以在使用文件IO时我们想要访问标准输入输出错误 就只需要直接使用文件描述符0、1、2即可

实际上关于文件描述符

但是如果我们使用标准IO该如何访问呢?

在头文件stdio.h中定义了三个流 标准输入流、标准输出流、标准错误流 如下

NAME
       stdin, stdout, stderr - standard I/O streams

SYNOPSIS
       #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

所以我们在使用标准IO时 直接使用这三个流就可以访问到标准输入标准输出和标准错误了。

标准输入、标准输出、标准错误是指在操作系统中,每个进程都会默认拥有的三个预定义的文件描述符(文件句柄),分别是0、1、2。其中,0表示标准输入(stdin)、1表示标准输出(stdout)、2表示标准错误(stderr)。这三个文件描述符的工作方式与普通的字符设备驱动是不同的。普通的字符设备驱动是指针对硬件设备的驱动程序,它们通过系统调用和设备文件进行交互,向设备发送命令或获取设备数据。而标准输入、标准输出、标准错误则是与进程直接相关的,它们是进程与操作系统之间的接口,用于向进程提供输入和输出功能。在具体的实现上,标准输入、标准输出、标准错误是由操作系统内核提供的,它们通过进程控制块(PCB)来与进程进行关联。当进程需要从标准输入读取数据时,它会调用系统调用来请求内核提供数据;当进程需要向标准输出或标准错误输出数据时,它会将数据写入到对应的文件描述符中,然后由内核负责将数据输出到终端或文件中。因此,标准输入、标准输出、标准错误的工作方式与普通的字符设备驱动是不同的。

/*************************************************************************************/

文件的阻塞和非阻塞

阻塞跟非阻塞都是属于文件的属性,并非调用函数的属性。
普通文件 ==> 非阻塞
设备文件 ==> 阻塞


普通文件:是默认不阻塞的!!!!


设备文件:/dev 是默认阻塞的!!!!
例如:管道、套接字默认阻塞



我们打开一个!设备文件!默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志。
open函数的O_NONBLOCK标志只适用于 设备文件  比如管道文件
对于普通文件是不适用的 如果想实现类似的功能可以使用文件锁
open函数如果没有使用O_NONBLOCK(非阻塞)模式    打开设备文件  则默认是阻塞的


使用read()函数读取设备文件的注意事项!!!

read读取设备文件时fd中的数据如果小于要读取的数据,就会引起阻塞。!!!

read读取普通文件就不会,无论读取到多少数据都会返回!

以下情况read不会引起阻塞:

(1)常规文件不会阻塞,不管读到多少数据都会返回;

(2)从终端读不一定阻塞:如果从终端输入的数据没有换行符,调用read读终端设备会阻塞,其他情况下不阻塞;

(3)从网络设备读不一定阻塞:如果网络上没有接收到数据包,调用read会阻塞,除此之外读取的数值小于count也可能不阻塞


1、打开文件


函数open()

    需要头文件:#include<sys/stat.h>

                         #include<fcntl.h>

    函数原型:int open(const char *pathname,int flags,int perms);

    函数参数:pathname:打开文件名(可以包含具体路径名)

                              flags:打开文件的方式,具体见下

                            perms:新建文件的权限,可以使用宏定义或者八进制文件权限码,具体见下

    函数返回值:成功:文件描述符

                        失败:-1

参数2flags具体可用参数(若使用多个flags参数可以使用|组合):

    O_RDONLY:以只读方式打开文件

    O_WRONLY:以只写方式打开文件

    O_RDWR:以可读可写方式打开文件

    O_CREAT:如果文件不存在,就创建这个文件,并使用参数3为其设置权限

    O_EXCL:如果使用O_CREAT创建文件时文件已存在则返回错误信息。使用这个参数可以测试文件是否已存在

    O_NOCTTY:若打开的是一个终端文件,则该终端不会成为当前进程的控制终端

    O_TRUNC:若文件存在,则删除文件中全部原有数据并设置文件大小为0

    O_APPEND:以添加形式打开文件,在对文件进行写数据操作时数据添加到文件末尾

    O_NONBLOCK:

(1)阻塞与非阻塞。如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。
(2)阻塞和非阻塞是两种不同的设计思路,并没有好坏。总的来说,阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但是结果没保障。
(3)操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的,所以我们应用程度调用这些函数的时候心里得非常清楚。
(4)我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志。
(5)只用于设备文件,而不用于普通文件。

open函数如果没有使用O_NONBLOCK(非阻塞)模式    打开设备文件  则默认是阻塞的

      O_SYNC:
(1)write阻塞等待底层完成写入才返回到应用层。
(2)无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不好等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。

    注意:O_RDONLY与O_WRONLY与O_RDWR三个参数互斥,不可同时使用

如果O_APPEND和O_TRUNC同时出现会怎么样?

O_APPEND起作用 O_TRUNC不起作用

若在参数2的位置有多个参数进行组合,注意使用按位或(|)运算符。

/** 可查看/usr/include/i386-linux-gnu/bits/fcntl.h文件看到具体的宏定义 **/

参数3perms表示新建文件的权限,可以使用宏定义或八进制文件权限码。其中宏定义的格式是:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X代表可读/可写/可执行,USR/GRP/OTH代表文件所有者/文件组/其他用户。例如:

S_IRUSR|S_IWUSR表示设置文件所有者具有可读可写权限,即0600。(一般情况下该参数都直接使用八进制文件权限码因为使用宏定义的形式太复杂)。

2、关闭文件

函数close()

    需要头文件:#include<unistd.h>

    函数原型:int close(int fd);

    函数参数:fd:文件描述符

    函数返回值:成功:0

                        失败:-1

exit、_exit、_Exit退出进程
(1)当我们程序在前面步骤操作失败导致后面的操作都没有可能进行下去时,应该在前面的错误监测中结束整个程序,不应该继续让程序运行下去了。
(2)我们如何退出程序?
第一种;在main用return,一般原则是程序正常终止return 0,如果程序异常终止则return -1。 但是return 只能在main函数中退出 其他函数就返回 
第一种:正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。

#include<stdio.h>
 
#include<stdlib.h>
 
#include<unistd.h>
 
#include<sys/stat.h>
 
#include<fcntl.h>
 
int main()
 
{
 
    int fd;
 
    if((fd=open("hello.txt",O_RDWR|O_CREAT|O_TRUNC,0666))<0)
 
    {
 
        perror("fail to open file");
 
        exit(0);
 
    }
 
    close(fd);
 
    return 0;
 
}

练习:说明以下在标准I/O中打开文件的模式所对应的在文件I/O中的模式(即flags的参数组合),其中文件名使用命令行传参的形式

例:w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

r

r+

w

w+

a

a+

答案:

r ----->  open(argv[1],O_RDONLY)

r+ ----> open(argv[1],O_RDWR)

w -----> open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666)

w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

a -----> open(argv[1],O_WRONLY|O_CREAT|O_APPEND,0666)

a+ ----> open(argv[1],O_RDWR|O_CREAT|O_APPEND,0666)

3、文件读写

read函数只是一个通用的读文件设备的接口。是否阻塞需要由设备的属性和设定所决定。

1.一般来说,读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。

2.如果是读普通文件,一般不会是阻塞方式的。但使用锁和fcntl设置取消文件O_NOBLOCK状态,也会产生阻塞的read效果
 


**函数read()**

    需要头文件:#include<unistd.h>

    函数原型:int read(int fd,void *buf,size_t count);

    函数参数:fd:文件描述符

                   buf:读取出的数据存放的缓冲区(内存地址)

                count:指定读取的字节数

    函数返回值:成功:读到的字节数count 0(表示文件已结尾)或其他情况

                        失败:-1

read读取普通文件就不会,无论读取到多少数据都会返回!

以下情况read不会引起阻塞:

(1)常规文件不会阻塞,不管读到多少数据都会返回;

(2)从终端读不一定阻塞:如果从终端输入的数据没有换行符,调用read读终端设备会阻塞,其他情况下不阻塞;

(3)从网络设备读不一定阻塞:如果网络上没有接收到数据包,调用read会阻塞,除此之外读取的数值小于count也可能不阻塞


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

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

  • 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。

函数write()

    需要头文件:#include<unistd.h>

    函数原型:ssize_t write(int fd,void *buf,size_t count);

    函数参数:fd:文件描述符

                   buf:待写入的数据存放的缓冲区

               count:指定写入的字节数

    函数返回值:成功:已写的字节数

                        失败:-1


示例:使用read()和write()函数,先向文件中写入一些数据,之后读取出来

#include<stdio.h>
 
#include<stdlib.h>
 
#include<strings.h>
 
#include<unistd.h>
 
#include<sys/stat.h>
 
#include<fcntl.h>
 
#define MAX 128
 
int main(int argc,char *argv[])
 
{
 
    int fdread,fdwrite;
 
    char readbuffer[MAX]={0},writebuffer[MAX];    //相当于自己设置了两块缓冲区
 
    if(argc<2)
 
    {
 
        perror("arguments are too few");
 
        exit(0);
 
    }
 
    //先打开文件写入数据
 
    if((fdwrite=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)
 
    {
 
        perror("fail to open file");
 
        exit(0);
 
    }
 
    printf("请输入写入的内容:");
 
    scanf("%[^\n]",writebuffer);
 
    write(fdwrite,writebuffer,MAX);
 
    close(fdwrite);
 
    //再打开文件读取刚写入的内容
 
    int n=0,sum=0;
 
    if((fdread=open(argv[1],O_RDONLY))<0)
 
    {
 
        perror("fail to open file");
 
        exit(0);
 
    }
 
    while((n=read(fdread,readbuffer,MAX))>0)
 
    {
 
        sum += n;
 
        printf("%s",readbuffer);
 
        bzero(readbuffer,MAX);
 
    }
 
    printf("共读取到%d个字节\n",sum);
 
    close(fdread);
 
    return 0;
 
}

执行示例程序后查看文件可以发现文件除了写入的字符外,还有部分乱码字符。这是因为writebuffer[]数组没有初始化,存储了部分随机数,未被数据覆盖掉的部分也同时被写入了文件中。

思考:如何解决这个问题?

答案是 用0初始化数组 %s在读取到‘ \0 ’(即0)就会停止

练习:使用文件I/O的read()/write()函数实现文件的复制

#include<stdio.h>
 
#include<stdlib.h>
 
#include<unistd.h>
 
#include<sys/stat.h>
 
#include<fcntl.h>
 
#define MAX 128
 
int main(int argc,char *argv[])
 
{
 
    int fdread,fdwrite;
 
    char buffer[MAX]={0};
 
    int n=0,sum=0;
 
    if(argc<3)
 
    {
 
        printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);
 
        exit(0);
 
    }
 
    if((fdread=open(argv[1],O_RDONLY))<0)
 
    {
 
        perror("fail to open file");
 
        exit(0);
 
    }
 
    if((fdwrite=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)
 
    {
 
        perror("fail to open file");
 
        exit(0);
 
    }
 
    while((n=read(fdread,buffer,MAX))>0)
 
    {
 
        sum += n;
 
        write(fdwrite,buffer,n);
 
    }
 
    printf("复制文件成功,共操作%d字节\n",sum);
 
    close(fdread);
 
    close(fdwrite);
 
    return 0;
 
}
 


 

4、文件定位


函数lseek()

    需要头文件:#include<unistd.h>

                         #include<sys/types.h>

    函数原型:off_t lseek(int fd,off_t offset,int whence);

    函数参数:        fd:文件描述符

                       offset:相对于基准点whence的偏移量(单位为字节),正数表示向后偏移负数表示向前偏移, 0  表示不移动

                    whence:基准点(取值同标准I/O内fseek()函数第三个参数)

    函数返回值:成功:返回结果偏移量  位置以字节为单位,从文件开始计算。  

                          失败:-1

其中第三个参数whence的取值如下:

        SEEK_SET:代表文件起始位置,数字表示为0

        SEEK_CUR:代表文件当前的读写位置,数字表示为1

        SEEK_END:代表文件结束位置,数字表示为2

lseek()仅将文件的偏移量记录在内核内而不进行任何I/O操作。

注意:lseek()函数仅能操作常规文件,一些特殊的文件(例如socket文件、管道文件等)无法使用lseek()函数。

我们可以通过:

n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小

示例:读取文件的最后10个字节的数据
 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<unistd.h>
 
#include<sys/stat.h>
 
#include<fcntl.h>
 
#define MAX 10
 
int main(int argc,char *argv[])
 
{
 
    int fd;
 
    char buffer[MAX]={0};
 
    int n=0,sum=0;
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n",argv[0]);
 
        exit(0);
 
    }
 
    if((fd=open(argv[1],O_RDONLY))<0)
 
    {
 
        perror("cannot open file");
 
        exit(0);3
 
    }
 
    lseek(fd,-10,SEEK_END);
 
    if(read(fd,buffer,MAX)>0)
 
        printf("读到的数据:%s\n",buffer);
 
    else
 
        printf("读取出错!\n");
 
    close(fd);
 
    return 0;
 
}

什么是文件空洞?

思考:若将基准点设置为SEEK_END但是偏移量是正数(即从文件末尾再向后偏移),会产生什么情况?

若将lseek()函数的基准点设置为SEEK_END但是偏移量是正数(即从文件末尾再向后偏移),则会产生“文件空洞”的情况。#但是不能从文件头向前偏移#

文件的偏移量是从文件开始位置开始计算的,若文件的偏移量大于了文件的实际数据长度,则会延长该文件,形成空洞。

示例:创建一个有空洞的文件。故意在文件结尾偏移好多个字节,然后再写入数据
 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<sys/stat.h>
 
#include<fcntl.h>
 
int main(int argc,char *argv[])
 
{
 
    int fd;
 
    int n;
 
    char buf1[]="LiLaoShiZhenShuai!";
 
    char buf2[]="ABCDEFG";
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n");
 
        exit(0);
 
    }
 
    if((fd=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666))<0)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    write(fd,buf1,strlen(buf1));//首先写入某些数据
 
    n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小
 
    printf("原先的文件大小是%d\n",n);
 
    n=lseek(fd,987654321,SEEK_END);//在文件末尾向后偏移很多字节
 
    printf("此时偏移量是%d\n",n);
 
    write(fd,buf2,strlen(buf2));//写入buf内数据
 
    n=lseek(fd,0,SEEK_END);
 
    printf("空洞后文件大小是%d\n",n);
 
    close(fd);
 
    return 0;
 
}

程序执行后,使用vim查看该文件,会发现在两段数据之间有一段乱码数据,并且使用ls -l指令查看,文件的大小也变大了。

    在UNIX/Linux文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为0,用read读取空洞部分读出的数据是0。空洞的部分用‘\0’即0代替,但是空洞并不占用磁盘块。

    空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务


七.文件锁

文件锁简介

文件锁(高级IO)【linux】(zzk)_Code_Beginner-CSDN博客文件锁文件锁的作用多进程读写文件写与写应该互斥读与写也应该是互斥的情况一情况二读与读共享文件锁读锁、写锁之间关系读锁和读锁共享读锁与写锁互斥写锁与写锁互斥文件锁的加锁方式对整个文件内容加锁对整个文件加锁是最常用的文件锁的加锁方式。对文件某部分内容加锁及就是加区域锁。文件锁的实现fcntl函数fcntl的函数原型功能返回值参数struct flock结构体代码演示文件锁的原理进程想加读锁进程想加写...https://skilledbeginner.blog.csdn/article/details/105605976

1、建议锁又称协同锁。对于这种类型的锁,内核只是提供加减锁以及检测是否加锁的操作,但是不提供锁的控制与协调工作。也就是说,如果应用程序对某个文件进行操作时,没有检测是否加锁或者无视加锁而直接向文件写入数据,内核是不会加以阻拦控制的。因此,建议锁,不能阻止进程对文件的操作,而只能依赖于大家自觉的去检测是否加锁然后约束自己的行为;

2、强制锁,是OS内核的文件锁。每个对文件操作时,例如执行open、read、write等操作时,OS内部检测该文件是否被加了强制锁,如果加锁导致这些文件操作失败。也就是内核强制应用程序来遵守游戏规则;

通过之前的open()/close()/read()/write()/lseek()函数已经可以实现文件的打开、关闭、读写等基本操作,但是这些基本操作是不够的。对于文件的操作而言,“锁定”操作是对文件(尤其是对共享文件)的一种高级的文件操作。当某进程在更新文件内数据时,期望某种机制能防止多个进程同时更新文件从而导致数据丢失,或者防止文件内容在未更新完毕时被读取并引发后续问题,这种机制就是“文件锁”。

    对于共享文件而言,不同的进程对同一个文件进行同时读写操作将极有可能出现读写错误、数据乱码等情况。在Linux系统中,通常采用“文件锁”的方式,当某个进程独占资源的时候,该资源被锁定,其他进程无法访问,这样就解决了共享资源的竞争问题。

    文件锁包括建议性锁(又名“协同锁”)强制性锁两种。建议性锁要求每个相关进程访问文件的时候检查是否已经有锁存在并尊重当前的锁(也可以不尊重)一般情况下不建议使用建议性锁,因为无法保证每个进程都能自动检测是否有锁,Linux内核与系统总体上都坚持不使用建议性锁。而强制性锁是由内核指定的锁,当一个文件被加强制性锁的过程中,直至该所被释放之前,内核将阻止其他任何进程对该文件进行读或写操作,每次读或写操作都得检测锁是否存在。当然,采用强制性锁对内核的性能影响较大,每次内核在操作文件的时候都需要检查是否有强制性锁。

    在Linux内核提供的系统调用中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件加建议性锁,这里不再讲解fcntl()函数既可以加建议性锁,也可以加强制性锁(默认为建议性锁)。同时,fcntl()还能对文件某部分上记录锁

强制性的锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他进程对其进行读写操作。采取强制性的锁对性能的影响很大,每次进行读写操作都必须检查是否有锁存在,fcntl默认是建议锁,如果想用强制锁,需要重新挂载mount系统分区,我们先看看fcntl函数

所谓记录锁,其实就是字节范围锁,它能锁定文件内某个特定区域,当然也可锁定整个文件。

记录锁又分为读锁写锁两种。

1)读锁又称为共享锁,它用来防止进程读取的文件记录被更改。记录内可设置多个读锁,但当有一个读锁存在的时候就不能在该记录区域设置写锁。读锁允许多个进程同时进行读操作,也称共享锁。文件加了读锁就不能再设置写锁,但仍允许其他进程在同一区域再设置读锁。

2)写锁又称为互斥锁,在任何时刻只能有一个程序对文件的记录加写锁,它用来保证文件记录被某一进程更新数据的时候不被其他进程干扰,确保文件数据的正确性,同时也避免其他进程“弄脏”数据。写锁的主要目的是隔离文件使所写内容不被其他进程的读写干扰,以保证数据的完整性。写锁一旦加上,只有上锁的人可以操作,其他进程无论读还是写只有等待写锁释放后才能执行,故写锁又称互斥锁,写锁与任何锁都必须互斥使用。

每当有系统调用 open()、read() 以及write() 发生的时候,内核都要检查,以验证其操作不会干扰由某个进程持有的某个锁对于通常的阻塞式描述符,与某个强制性锁冲突的write或read讲把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述符,将会返回一个EAGAIN错误。

文件记录一旦被设置写锁,就不能再设置任何锁直至该写锁解锁。

文件锁其它值得注意的地方
(a)在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符,那么该进程加在文件上的文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。

进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁

(b)父进程所加的文件锁,子进程不会继承,我们在讲进程控制时就说过加锁是进程各自私人事情,不能继承,就好比你老爸有抽烟的嗜好,难道这也需要继承吗,肯定不是的。

多线程间能不能使用fcntl实现的文件锁呢?
可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述符才有效。
 

对比进程信号量

1)进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作。
2)文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作。


文件锁编程

1)fcntl()函数

需要头文件:#include<unistd.h>

                        #include<sys/types.h>

                        #include<fcntl.h>

    函数原型

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

    函数参数:fd:文件描述符

              cmd:检测锁或设置锁(这些命令是int类型)

              lock_set:结构体类型指针,结构体struct flock需要事先设置,与第二个参数连用

    函数返回值:失败返回-1

                        成功则返回某个其他值或0

  F_DUPFD  The new descriptor.

  F_GETFD  Value of file descriptor flags.  

F_GETFL  Value of file status flags.  

F_GETLEASE Type of lease held on file descriptor.

F_GETOWN Value of descriptor owner.  

F_GETSIG Value of signal sent when read or write becomes  possi‐ ble, or zero for traditional SIGIO behavior.  

F_GETPIPE_SZ, F_SETPIPE_SZ The pipe capacity.  

F_GET_SEALS A bit mask identifying the seals that have been set for the inode referred to by fd.

这8个cmd返回特殊值 其他cmd 成功返回0

fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:

(1)F_DUPFD

与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。

(2)F_GETFD

读取文件描述符close-on-exec标志

(3)F_SETFD

将文件描述符close-on-exec标志设置为第三个参数arg的最后一位

(4)F_GETFL

获取文件打开方式的标志,标志值含义与open调用一致

(5)F_SETF

设置文件打开方式为arg指定方式

(6)F_SETLK

此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁。

如果锁被其他进程占用,则返回-1;

这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。

(7)F_SETLKW

此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放。

也就是说这个命令 在文件已经有锁的情况下  如果我们想设置的新锁与之前的锁冲突 那么会等待之前的锁释放后再 让该文件上我们设置的新锁

(8)F_GETLK:测试由lock所描述的锁是否能使用

第3个参数lock指向一个希望设置的锁的属性结构

1)如果锁能被设置,F_GETLK该命令并不真的设置锁,而是只修改lock指向的结构体的l_type为F_UNLCK(既无锁)然后除了将l_type设置为F_UNLCK之外,lock所指向的结构中的其他信息保持不变。

2)如果fcntl()第一个参数fd代表的文件已经存在一个或多个锁与第三个参数lock指向的结构体中希望设置的锁(short_l_type)相互冲突,则将把现存锁的信息写到fcntl()第三个参数lock指向的结构中(l_type-已有锁的类型,l_pid-加锁的进程号)。

所以可以用这个命令来判断我们想操作的这个文件(也就是fcntl第一个参数fd所代表的文件)是否已经有锁了


文件记录锁是fcntl函数的主要功能。

记录锁:实现只锁文件的某个部分,并且可以灵活的选择是阻塞方式还是立刻返回方式

当fcntl用于管理文件记录锁的操作时,第三个参数指向一个struct flock *lock的结构体

struct flock
{
    short l_type;    /*锁的类型*/
    short l_whence;  /*偏移量的起始位置:SEEK_SET,SEEK_CUR,SEEK_END*/
    off_t l_start;     /*加锁的起始偏移*/
    off_t l_len;    /*上锁字节*/
    pid_t l_pid;   /*锁的属主进程ID */
}; 

注意:必须定义struct flock类型结构体并初始化结构体内的数据,然后使用地址传递的方式传递参数,不允许直接定义struct flock* 类型指针直接传参
 

结构体成员说明:

    l_type:有三个参数

            F_RDLCK:读锁(共享锁)

            F_WRLCK:写锁(排斥锁)

            F_UNLCK:无锁/解锁

    l_whence:相对于偏移量的起点,参数等同于fseek()与lseek()中的whence参数

            SEEK_SET:位置为文件开头位置

            SEEK_CUR:位置为文件当前读写位置

            SEEK_END:位置为文件结尾位置

    l_start:加锁区域在文件中的相对位移量,与l_whence的值共同决定加锁区域的起始位置

    l_len:加锁区域的长度,若为0则表示直至文件结尾EOF

    l_pid:具有阻塞当前进程的锁,其持有的进程号会存放在l_pid中,仅由F_GETLK返回

思考:如何设置该结构体内的成员使得加锁的范围为整个文件?

答案:设置l_whence为SEEK_SET,l_start为0,l_len为0即可。


示例:使用fcntl()函数对文件进行锁操作。首先初始化结构体flock中的值,然后调用两次fcntl()函数。第一次参数设定为F_GETLK判断是否可以执行flock内所描述的锁操作;第二次参数设定为F_SETLK或F_SETLKW对该文件进行锁操作。

注意:需要至少两个终端运行该程序才能看到效果
 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<unistd.h>
 
#include<fcntl.h>
 
#include<sys/types.h>
 
int lock_set(int fd,int type)
 
{
 
    struct flock lock;
 
    lock.l_whence = SEEK_SET;
 
    lock.l_start = 0;
 
    lock.l_len = 0;//三个参数设置锁的范围是全文件
 
    lock.l_type = type;//type的参数由主调函数传参而来
 
    lock.l_pid = -1;
 
    
 
    //第一次操作,判断该文件是否可以上锁
 
    fcntl(fd,F_GETLK,&lock);
 
    if(lock.l_type!=F_UNLCK)//如果l_type得到的返回值不是F_UNLCK则证明不能加锁,需判断原因
 
    {
 
        if(lock.l_type==F_RDLCK)
 
        {
 
            printf("This is a ReadLock set by %d\n",lock.l_pid);
 
        }
 
        else if(lock.l_type==F_WRLCK)
 
        {
 
            printf("This is a WriteLock set by %d\n",lock.l_pid);
 
        }
 
    }
 
    
 
    //第二次操作,对文件进行相应锁操作 F_SETLKW如果文件有锁等锁释放再上锁
 
    lock.l_type = type;
 
    if((fcntl(fd,F_SETLKW,&lock))<0)
 
    {
 
        printf("Lock Failed:type = %d\n",lock.l_type);
 
        return -1;
 
    }
 
    switch(lock.l_type)
 
    {
 
        case F_RDLCK:
 
            printf("ReadLock set by %d\n",getpid());break;
 
        case F_WRLCK:
 
            printf("WriteLock set by %d\n",getpid());break;
 
        case F_UNLCK:
 
            printf("ReleaseLock by %d\n",getpid());
 
            return 1;
 
            break;
 
    }
 
    return 0;
 
}
 
int main(int argc, const char *argv[])
 
{
 
    int fd;
 
    if((fd=open("hello.txt",O_RDWR))<0)
 
    {
 
        perror("fail to open hello.txt");
 
        exit(0);
 
    }
 
    printf("This pid_no is %d\n",getpid());
 
    //给文件上锁
 
    lock_set(fd,F_WRLCK);
 
    printf("Press ENTER to continue...\n");
 
    getchar();
 
    //给文件解锁
 
    lock_set(fd,F_UNLCK);
 
    close(fd);
 
    return 0;
 
}

 这个例子 中使用fcntl()给文件加写锁 在未解锁情况下如果我们使用另外一个进程 向这个文件写入数据 发现可以正常写入 这是因为 fcntl默认是建议锁,如果想用强制锁,需要重新挂载mount系统分区

好了,下面来看一个例子:


我们用一个死循环来防止这个进程死掉


这样这个进程就一直在执行,我们新开一个远程端,来修改hello.txt这个文件

我们发现还是可以修改这个文件,这是因为fcntl函数默认是建议锁,如果想用强制锁呢,需要重新mount分区


我们再来跑一下程序

  
再在另外一个远程终端修改hello.txt


我们发现他阻塞在这里了,也就是暂时不能修改了,这个时候我强制终止前一个进程

 


再来看另外一个远程终端


发现已经修改了。


2)flock()函数

flock只能给整个文件上锁不能局部上锁

定义函数 int flock(int fd,int operation);

// Apply or remove an advisory lock on the open file specified by fd

参数 operation有下列四种情况:

LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。

LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。

LOCK_UN 解除文件锁定状态。

LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。

单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。

返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。

flock只要在打开文件后,需要对文件读写之前flock一下就可以了,用完之后再flock一下,前面加锁,后面解锁。

进程使用flock尝试锁文件时,如果文件已经被其他进程锁住,进程会被阻塞直到锁被释放掉,或者在调用flock的时候,采用LOCK_NB参数,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误。

flock锁的释放非常具有特色,即可调用LOCK_UN参数来释放文件锁,也可以通过关闭fd的方式来释放文件锁(flock的第一个参数是fd),意味着flock会随着进程的关闭而被自动释放掉

当使用LOCK_EX 排他锁时,同一时刻只能有一个进程锁定成功,其余进行只能阻塞,这种行为与多线程读写锁中的写锁类似。

//lock
if ((lock_fd = open(lock_path, O_CREAT|O_WRONLY, 0664)) < 0) {
		LOG("Failed to open %s: %m\n", PATH);
		return -1;
	}
 
	if (flock(lock_fd, LOCK_EX) < 0) {
		LOG("Failed to lock\n");
		return -1;
	}
 
//unlock
close(lock_fd);


 


flock, lockf, fcntl
这三个函数的作用都是给文件加锁,那它们有什么区别呢?

首先flock和fcntl是系统调用,而lockf是库函数。lockf实际上是fcntl的封装,所以lockf和fcntl的底层实现是一样的,对文件加锁的效果也是一样的。后面分析不同点时大多数情况是将fcntl和lockf放在一起的。

关于flock函数,首先要知道flock函数只能对整个文件上锁,而不能对文件的某一部分上锁,这是于fcntl/lockf的第一个重要区别,后者可以对文件的某个区域上锁。

其次,flock只能产生劝告性锁。我们知道,linux存在强制锁(mandatory lock)和劝告锁(advisory lock)。所谓强制锁,比较好理解,就是你家大门上的那把锁,最要命的是只有一把钥匙,只有一个进程可以操作。所谓劝告锁,本质是一种协议,你访问文件前,先检查锁,这时候锁才其作用,如果你不那么kind,不管三七二十一,就要读写,那么劝告锁没有任何的作用。而遵守协议,读写前先检查锁的那些进程,叫做合作进程。

再加上,flock可以有共享锁和排它锁,lockf只支持排它锁,但是fcntl里面参数flock可以有RDLCK读锁。

再次,flock和fcntl/lockf的区别主要在fork和dup时候的区别,后面有讲。

另外,flock不能再NFS文件系统上使用,如果要在NFS使用文件锁,请使用fcntl。
 


八.Linux标准IO

学习标准IO前需要知道的一些东西

    标准I/O的核心对象是流当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件。标准I/O函数都是基于流进行各种操作的。

fd只是一个小整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。 

文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用; 

而流(如: fopen)返回的是一个FILE结构指针, FILE结构是包含有文件描述符的FILE结构函数可以看作是对fd直接操作的系统调用的封装, 它的优点是带有I/O缓存。 

C语言的stdio.h头文件中,定义了用于文件操作的结构体FILE。这样,我们通过fopen返回一个文件指针(指向FILE结构体的指针)来进行文件操作。可以在stdio.h头文件中查看FILE结构体的定义

搞清楚FILE结构体和file结构(打开文件表)的区别!!!!!

FILE是类似于C语言构建的类似于file结构(打开文件表)的一个结构体 此文件指针FILE * 和前面文件描述符表中的文件指针不是同一个!!!!

每个进程在PCB(Process Control Block)即进程控制块中都保存着一份文件描述符表,文件描述符就是这个表的索引,文件描述表中每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体(打开文件表)表示,文件描述符表中的指针指向file结构体(file结构体就是打开文件表)。 

ANSI标准C库函数是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。


文件IO读写位置(既文件偏移量)和使用C标准I/O库时的读写位置有可能不同,文件IO读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区(既流缓冲区)中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。


1、标准I/O定义


    标准I/O指的是ANSI C中定义的用于I/O操作的一系列函数。只要操作系统安装了C库,就可以调用标准I/O。换句话说,若程序使用标准I/O函数,那么源代码无需进行任何修改就可以在其他操作系统上编译,具有更好的可移植性。这些标准IO函数其实是由文件IO封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。

    除此之外,由于标准I/O封装了缓冲区,使得在读写文件的时候减少了系统调用的次数,提高了效率。在执行系统调用的时候(比如文件IO),Linux必须从用户态切换到内核态,在内核中处理相应的请求,然后再返回用户态。如果频繁地执行系统调用则会增加这种开销。标准I/O为了减少这种开销,采取缓冲机制,为用户空间创建缓冲区,读写时优先操作缓冲区,在必须访问文件时(例如缓冲区满、强制刷新、文件读写结束等情况)再通过系统调用将缓冲区的数据读写实际文件中,从而避免了系统调用的次数。

2、流的定义

    流操作函数对象不是文件描述符,而是一个流缓冲区(就是在使用标准IO是我们常说的缓冲区)
    标准I/O的核心对象是流当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件。标准I/O函数都是基于流进行各种操作的。

/**********************流的分类***********************/

流的分类分为文本流和二进制流两种

    文本流:文本流是由字符文件组成的序列,每一行包含0个或多个字符并以'\n'结尾。在流处理过程中所有数据以字符形式出现,'\n'被当做回车符CR和换行符LF两个字符处理,即'\n'ASCII码存储形式是0x0D和0x0A。当输出时,0x0D和0x0A转换成'\n'

    二进制流:二进制流是未经处理的字节组成的序列,在流处理过程中把数据当做二进制序列,若流中有字符则把字符当做ASCII码的二进制数表示。'\n'不进行变换。

例如:2016在文本流中和二进制流中的数据类型不同:

文本流:2016---->'2''0''1''6'---->50 48 49 54

二进制流:2016-->数字2016--->0000011111010001

/**********************流的分类end********************/

在使用标准I/O操作文件的时候,每个被程序使用的文件都会在内存中开辟一块区域,用来存放与文件相关的属性信息,这些信息存放在一个FILE类型的结构体中,FILE类型的结构体是由C定义的一个结构体:
 

typedef struct
 
{
 
    short level;                //缓冲区满/空的状态
 
    unsigned flags;             //文件状态标志
 
    char fd;                    //文件描述符
 
    unsigned char hold;         //如缓冲区无内容则不读取字符
 
    short bsize;                //缓冲区的大小
 
    unsigned char *buffer;      //数据缓冲区的位置
 
    unsigned char *curp;        //指针当前的指向
 
    unsigned istemp;            //临时文件指示器
 
    short token;                //用于有效性检查
 
}FILE;

在标准I/O中,常用FILE类型的结构体指针FILE*来操作文件。


缓冲区机制

流操作函数对象不是文件描述符,而是一个流缓冲区(就是在使用标准IO是我们常说的缓冲区)

根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作:

  ① 带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。

  ② 非缓冲区文件操作:低级文件I/O操作,读写文件时,不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),当然用于可以在自己的程序中为每个文件设定缓冲区。

两种文件操作的解释和比较:

  (1)非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

  (2)ANSI标准C库函数是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。

缓冲类型

在标准I/O中预定义了三块缓冲区:stdin、stdout、stderr,分别代表标准输入流、标准输出流、标准输出错误流。见下:

流的名称        程序中使用

标准输入           stdin    

标准输出           stdout    

标准错误输出    stderr    

标准I/O中的流的缓冲类型有三种

1.全缓冲:这种情况下,当缓冲区被填满后才进行实际的I/O操作。对于存放在磁盘上的普通文件用标准I/O打开时默认是全缓冲的当缓冲区满或者执行刷新缓冲区(fflush)操作才会进行磁盘操作。

2.行缓冲:这种情况下,当在输入/输出中遇到换行符时执行I/O操作。-->标准输入/输出流(stdin/stdout)就是使用行缓冲。

3.无缓冲:不使用缓冲区进行I/O操作,即对流的读写操作会立即操作实际文件。标准出错流(stderr)是不带缓冲的,这就使得当发生错误时,错误信息能第一时间显示在终端上,而不管错误信息是否包含换行符。

示例1.1:stdout使用行缓冲形式

效果:不会立即打印内容,而是等待'\n'或者缓冲区满才输出。

#include<stdio.h>
 
int main()
 
{
 
    while(1)
 
    {
 
        printf("Hello World");
 
        sleep(1);//延时1秒
 
    }
 
    return 0;
 
}

当缓冲区满的时候才输出到屏幕

示例1.2:stdout使用行缓冲形式

效果:当添加了'\n'之后,会正确地输出

#include<stdio.h>
 
int main()
 
{
 
    while(1)
 
    {
 
        printf("Hello World\n");
 
        sleep(1);//延时1秒
 
    }
 
    return 0;
 

每1S输出一次

示例1.3:stderr使用无缓冲形式

效果:stderr使用无缓冲,即使不使用'\n'仍能立即输出

#include<stdio.h>
 
int main()
 
{
 
    while(1)
 
    {
 
        perror("Hello World");   //无缓冲
 
        sleep(1);//延时1秒
 
    }
 
    return 0;
 
}

perror会自动打印系统的提示信息 (:Success) 

示例2:编写程序实现以下功能:

    ⒈向标准输出流输出HelloWorld

    ⒉向标准错误流输出HelloWorld

    ⒊控制输出重定向,使程序仅能输出标准输出流的字符

    ⒋控制输出重定向,使程序仅能输出标准错误流的字符

#include<stdio.h>
 
int main()
 
{
 
    fprintf(stdout,"%s","This is stdout:HelloWorld!\n");
 
    fprintf(stderr,"%s","This is stderr:HelloWorld!\n");
 
    //fprintf的作用是向某个流(文件)中按格式输出指定内容
 
    return 0;
 
}

实现第三步的功能:在执行时:./a.out 2> /dev/null

仅能显示标准输出的内容

实现第四步的功能:在执行时:./a.out 1> /dev/null

仅能显示标准出错的内容


九.标准I/O编程

/*************************************************************************************/

我们知道对于每个进程都会有一个进程控制块(PCB,在内核中使用数据结构task_struct)来描述,在每个进程的PCB中都会有一个文件描述符表(这是个数组) 用来表示这个进程打开的文件。

其中文件描述符表的第0、1、2个元素 也就是值为0、1、2的文件描述符是固定的 分别代表为:标准输入、标准输出、标准错误。也就是说 Linux中一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这三个文件分别对应文件描述符0、1、2。

所以在使用文件IO时我们想要访问标准输入输出错误 就只需要直接使用文件描述符0、1、2即可

但是如果我们使用标准IO该如何访问呢?

在头文件stdio.h中定义了三个流 标准输入流、标准输出流、标准错误流 如下

NAME
       stdin, stdout, stderr - standard I/O streams

SYNOPSIS
       #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

所以我们在使用标准IO时 直接使用这三个流就可以访问到标准输入标准输出和标准错误了。

标准输入、标准输出、标准错误是指在操作系统中,每个进程都会默认拥有的三个预定义的文件描述符(文件句柄),分别是0、1、2。其中,0表示标准输入(stdin)、1表示标准输出(stdout)、2表示标准错误(stderr)。这三个文件描述符的工作方式与普通的字符设备驱动是不同的。普通的字符设备驱动是指针对硬件设备的驱动程序,它们通过系统调用和设备文件进行交互,向设备发送命令或获取设备数据。而标准输入、标准输出、标准错误则是与进程直接相关的,它们是进程与操作系统之间的接口,用于向进程提供输入和输出功能。在具体的实现上,标准输入、标准输出、标准错误是由操作系统内核提供的,它们通过进程控制块(PCB)来与进程进行关联。当进程需要从标准输入读取数据时,它会调用系统调用来请求内核提供数据;当进程需要向标准输出或标准错误输出数据时,它会将数据写入到对应的文件描述符中,然后由内核负责将数据输出到终端或文件中。因此,标准输入、标准输出、标准错误的工作方式与普通的字符设备驱动是不同的。

/*************************************************************************************/

  流操作函数对象不是文件描述符,而是一个流缓冲区!!!!!!

1、打开文件(流)


使用fopen()/fdopen()/freopen()函数可以打开一个文件。其中fopen()是最常用的函数,fdopen()可以指定打开文件的文件描述符和模式,freopen()除可以指定打开的文件与模式外,还可以指定特定的I/O流。

函数fopen()

             fopen做的事:找到文件,打开文件,并将文件内的数据加载到流内

    需要头文件:#include<stdio.h>

    函数原型:FILE *fopen(const char *path,const char *mode)

    函数参数:path:要打开的文件的路径及文件名

                   mode:文件打开方式,见下

    函数返回值:成功:指向文件的FILE类型指针

                        失败:NULL

以下是mode参数允许使用的取值及说明:

    r或rb               以只读的方式打开文件,该文件必须存在

    r+或r+b          以可读可写的方式打开文件,若文件不存在则创建该文件;若文件存在则擦除文件原始内容,从文件开头开始操作文件

    w或wb            以只写的方式打开文件,该文件必须存在

    w+或w+b       以可读可写的方式打开文件,若文件不存在则创建该文件;若文件存在则擦除文件原始内容,从文件开头开始操作文件

    a或ab              以附加的方式打开只写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留

    a+或a+b         以附加的方式打开可读可写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留

 

如果读写的是二进制文件,则还要加b,比如rb、r+b等。unix\linux不区分文本和二进制文件 

注意:

    ⒈ +   的作用代表操作文件的方式是只读/写/附加(无+)还是同时读写(有+)

    ⒉ b   的作用代表操作的文件是ASCII文本文件(无b)还是二进制文件(有b)

r+: Open for reading and writing.  The stream is positioned  at  the beginning of the file.

w+:Open for reading and writing.  The file is created  if  it  does not  exist, otherwise it is truncated.  The stream is positioned at the beginning of the file.

r+具有读写属性,从文件头开始写,保留原文件中没有被覆盖的内容;

w+具有读写属性,写的时候如果文件存在,会被清空,从头开始写。

r 打开只读文件,该文件必须存在。 
r+ 打开可读写的文件,该文件必须存在。 
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。 
a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 
上述的形态字符串都可以再加一个b字符,如rb、w+b或ab+等组合,加入b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。不过在POSIX系统,包含Linux都会忽略该字符。

2、关闭文件(流)

fclose()函数


使用fclose()函数可以关闭一个文件,该函数将缓冲区内的所有内容写入相关文件中并回收相应的系统资源    这是因为存放在磁盘的普通文件是全缓冲的 只有缓冲区满或者刷新缓冲区才会进行磁盘操作  这里关闭文件就相当于刷新缓冲区了

函数fclose()

    需要头文件:#include<stdio.h>

    函数原型:int fclose(FILE *stream)

    函数参数:stream:已打开的流指针

    函数返回值:成功:0

                        失败:EOF

示例:打开一个文件然后关闭

写一个程序,打开一个文件,然后关闭该文件。
 

#include<stdio.h>
 
#include<stdlib.h>
 
int main()
 
{
 
    FILE *fp;
 
    if((fp = fopen("hello.txt","w"))==NULL)//打开文件,之后判断是否打开成功
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    //对文件的操作
 
    fclose(fp);//关闭文件
 
    return 0;
 
}

注意:由于打开文件fopen()函数可能会失败,所以我们打开文件后通常需要判断fopen()函数是否打开文件成功。判断的方法是将fopen的结果放入if()表达式中并判断该表达式得到的结果是否为NULL(空指针)。

练习:若示例程序的hello.txt文件不存在,以"r"的模式打开该文件会出现什么效果?

答案:
 

#include<stdio.h>
 
#include<stdlib.h>
 
int main()
 
{
 
    FILE *fp;
 
    if((fp = fopen("hello.txt","r"))==NULL)//打开文件,之后判断是否打开成功
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    //对文件的操作
 
    fclose(fp);//关闭文件
 
}

若hello.txt文件不存在,则在执行程序时会报错:

示例:打开文件hello.txt,使用fprintf()向hello.txt中写入"HelloWorld"。其中打开文件部分使用命令行传参。

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc,char *argv[])
 
{
 
    FILE *fp;
 
    if((fp = fopen(argv[1],"w"))==NULL)//打开文件,之后判断是否打开成功
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fprintf(fp,"%s","HelloWorld\n");
 
    fclose(fp);//关闭文件
 
}
 

练习1:打开文件hello.txt,使用fprintf()在刚才的"HelloWorld"之后添加一行"NanJing"

答案:

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, char *argv[])
 
{
 
    FILE *fp;
 
    if((fp = fopen(argv[1],"a"))==NULL)//打开文件,之后判断是否打开成功
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fprintf(fp,"%s","NanJing\n");
 
    fclose(fp);//关闭文件
 
}
 

练习2:打开文件hello.txt,将该文件原始内容清空并添加"Zhen Mei!"


3、错误输出


在刚才的示例程序与练习程序中使用了perror这个函数。perror函数可以在程序出错的时候将错误信息输出到标准错误流stderr中。由于标准错误流不使用缓冲所以可以及时显示错误信息。

函数perror()

    需要头文件:#include<stdio.h>

    函数原型:void perror(const char *s)

    函数参数:s:在标准错误流上输出的错误信息

    函数返回值:无

在标准I/O函数执行的时候,若发生了错误(例如以r或r+打开一个不存在的文件),会将错误码保存在系统变量errno中。使用perror函数可以检测errno的错误码信息并输出对应的错误信息。

注意:errno的变量声明不在stdio.h中,而是在头文件errno.h中

除了使用perror来输出错误信息,也可以使用strerror函数手动获得错误码对应的错误信息

函数strerror()

    需要头文件:#include<string.h>、#include<errno.h>

    函数原型:char *strerror(int errno)

    函数参数:errno:返回的错误码

    函数返回值:错误码对应的信息

示例:不使用perror,使用strerror输出错误信息
 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<errno.h>
 
int main(int argc, char *argv[])
 
{
 
    FILE *fp;
 
    fp = fopen(argv[1],"r");
 
    if(NULL == fp)
 
    {
 
        printf("Open %s was error:%s\n",argv[1],strerror(errno));
 
        return 1;
 
    }
 
    printf("Open %s wasn't error:%s\n",argv[1],strerror(errno));
 
    fclose(fp);
 
    return 0;
 
}
 

 


4、按字符输入/输出


函数getc()、fgetc()、getchar()

    需要头文件:#include<stdio.h>

    函数原型:int getc(FILE *stream)

                     int fgetc(FILE *stream)

                     int getchar(void)

    函数参数:stream:输入文件流

    函数返回值:成功:读到的字符

                        失败:EOF

/*********************有关EOF************************/

在C语言(或者更精确的说是在C标准库中)EOF表示一个文本文件的结束符(end of file),这个宏定义在头文件stdio.h中,其值为-1,因为在ASCII表的编码(0~255)中没有-1编码。EOF通常被当做文件结束的标志,还有很多的文件处理相关的函数使用EOF作为函数出错的返回值。

但是要注意的是,EOF只能作为文本文件(流)的结束符,因为若该文件(流)是二进制形式文件则会有-1的出现,因此无法使用EOF来表征文件结束。为解决这个问题,在C语言中提供了一个feof()函数,若遇到文件结尾,函数feof()返回1,否则返回0。这个函数既可以判断二进制文件也可以判断文本文件。

/*********************有关EOFend*********************/

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

   

 函数putc()、fputc()、putchar()

    需要头文件:#include<stdio.h>

    函数原型:int putc(int c,FILE *stream)

                     int fputc(int c,FILE *stream)

                     int putchar(int c)

    函数参数:c:待输出的字符(的ASCII码)

            stream:输入文件流

    函数返回值:成功:输出字符c

                        失败:EOF

putc()函数和fputc()函数是从一个指定的流中输出一个字符,putchar()函数是从stdout中输出一个字符。
示例:从文件hello.txt中读取字符然后输出到显示器上

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, char *argv[])
 
{
 
    int c;
 
    FILE *fp;
 
    fp = fopen(argv[1],"r+");
 
    if(NULL == fp)     //判断文件是否打开成功
 
    {
 
        perror("Can't open this file");
 
        return 1;
 
    }
 
    c = fgetc(fp);
 
    while(c != EOF)
 
    {
 
        putchar(c);
 
        c = fgetc(fp);
 
    }
 
    fclose(fp);
 
    return 0;
 
}

练习:文件hello.txt中存放了各种字符(大写字母、小写字母、数字、特殊符号等),将该文件中的字母输出,非字母不输出

答案:

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, char *argv[])
 
{
 
    int c;
 
    FILE *fp;
 
    fp = fopen(argv[1],"r+");
 
    if(NULL == fp)
 
    {
 
        perror("Can't open this file");
 
        return 1;
 
    }
 
    c = fgetc(fp);
 
    while(EOF != c)
 
    {
 
        if((c >= 'A'&&c <= 'Z')||(c >= 'a'&&c <= 'z'))
 
        putchar(c);
 
        c = fgetc(fp);
 
    }
 
    printf("\n");
 
    fclose(fp);
 
    return 0;
 
}
 

 

这里和下面学的输入输出函数都是对于流来说的

5、fgets()-- 按行字符输入/输出

fgets()会在读取到的数据末尾补上一个 ' \0 ' !

比如 char *fgets(char *s,int size,FILE *stream)是获取流(FILE *stream)中的数据 放到char *s中

get就是获取流的数据 就是对于流来说的


当然,如果我们每次都按字符一个一个字符操作的话,程序执行效率会大大降低。因此标准I/O中提供了按行输入/输出的操作函数。

    函数gets()、fgets()

    需要头文件:#include<stdio.h>

    函数原型:char *gets(char *s)

                     char *fgets(char *s,int size,FILE *stream)

    函数参数:s:存放输入字符的缓冲区地址(不是流缓冲区 不要搞混了)

                 size:输入的字符串长度

            stream:输入文件流

    函数返回值:成功:s

                        失败或读到文件尾:NULL

在Linux的内核man手册中,对gets()函数的评价是:"Never use gets().  Because it is impossible to tell without knowing the data in advance how many  characters  gets()  will  read,  and  because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use.  It has  been  used  to  break  computer security.  Use fgets() instead."简单来说gets()的执行逻辑是寻找该输入流的'\n'并将'\n'作为输入结束符,但是若输入流数据超过存储空间大小的话会覆盖掉超出部分的内存数据,因此gets()函数十分容易造成缓冲区的溢出,不推荐使用。而fgets()函数的第二个参数指定了一次读取的最大字符数量。

举个例子:

char ch[10]; int mode=10;(假设fp已经分配好了)
fgets(ch,10,fp);
这个函数表示从fp这个文件指针对应的缓冲区中读取10个字符,要注意这10个字符是包括了fgets在末尾补上的' \0 '的!

所以

1.如果fp对应的缓冲区中的数据为 1234\n 。fgets(ch,10,fp);读取到字符数组ch中的数据 就会是 1234\n\0

2.如果fp对应的缓冲区中的数据为 123456789z\n 。fgets(ch,10,fp);读取到字符数组ch中的数据 就会是 123456789\0

显然造成12中读取到的数据的区别是因为 fgets的第二个参数和fp对应的缓冲区中数据的长度。1中的缓冲区数据较短读到行尾时都没有达到size-1的字节数 所以会将行尾的换行符\n读入且在末尾加上\0。而2中的缓冲区较长 读到size-1的字节数后就直接截断 并且在末尾补上一个\0然后写入到ch字符数组中

因为fgets()会在读取到的数据末尾补上一个 ' \0 '所以我们可以直接使用strlen()来获取读取到的数据长度 这对我们后续想要进行的操作非常方便!

当fgets()读取到'\n'或已经读取了size-1个字符后就会返回,并在整个读到的数据后面添加'\0'作为字符串结束符 !

(因此fgets()的读取大小保证了不会造成缓冲区溢出,但是也意味着fgets()函数可能不会读取到完整的一行(即可能无法读取该行的结束符'\n'))

意思就是如果使用fgets读取指定流中的数据:

1.如果 fgets的第二个参数 也就是指定读的字节数size太少 也就是小于或等于一行 就会直接截断读取前size-1个字节的数据且在第size个字节的位置补上个' \0 '  然后保存到fgets的第一个参数中 

2.如果 fgets的第二个参数 也就是指定读的字节数size足够大 也就是size大于一行 就会把这一行的数据包括换行符\n 保存到第一个参数中

(注意:这里的一行数据是包括换行符\n和它前面的数据的!)

//学习过fgets()之后,输入字符串尽可能多使用fgets(),而尽量避免使用gets()和scanf("%[^\n]")

示例:使用fgets()函数,依次读取文件内的内容并输出。
 

#include<stdio.h>
 
#include<stdlib.h>
 
 
 
#define MAX 128
 
 
 
int main(int argc, char *argv[])
 
{
 
    int c;
 
    char buf[MAX] = {0};  //初始化数组:清零
 
    FILE *fp;
 
    if(argc < 2)
 
    {
 
        printf("Arguments are too few\n");
 
        return 1;
 
    }
 
    fp = fopen(argv[1],"r+");
 
    if(NULL == fp)
 
    {
 
        perror("Can't OPen file");
 
        return 1;
 
    }
 
    while(fgets(buf,MAX,fp) != NULL)
 
    {
 
        printf("%s",buf);
 
    }
 
    fclose(fp);
 
    return 0;
 
}

思考:由于fgets()函数不一定会读取到'\n',那么如何使用fgets()函数来统计一个文件有多少行呢?

练习:使用fgets()函数统计某个文本文件有多少行

#include<stdio.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#define MAX 128
 
int main(int argc,char *argv[])
 
{
 
    int c;
 
    char buf[MAX]={0};
 
    FILE *fp;
 
    int line=0;
 
    if(argc<2)
 
    {
 
        perror("argument is too few");
 
        exit(0);
 
    }
 
    if((fp = fopen(argv[1],"r+"))==NULL)//打开文件,之后判断是否打开成功
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    while(fgets(buf,MAX,fp)!=NULL)
 
    {
 
        if(buf[strlen(buf)-1]=='\n')//若这次读取读到了'\n',则证明该行结束
 
            line++;//行数+1
 
    }
 
    printf("This file %s has %d line(s)\n",argv[1],line);
 
    fclose(fp);
 
    return 0;
 
}
 

注意: strlen不会计算 ' /0 ' 的长度 !

函数puts()、fputs()

    需要头文件:#include<stdio.h>

    函数原型:int puts(const char *s)

              int fputs(conse char *s,FILE *stream)

    函数参数:s:存放输出字符的缓冲区地址(不是流缓冲区 不要搞混了)

            stream:输出文件流

    函数返回值:成功:非负数

                        失败:EOF
 


6、使用格式化输入/输出操作文件

在学习fprintf和fscanf前 我们在之前学习c语言时就学过了printf和scanf

printf的作用是将数据打印到标准输出(显示器)其实就是将数据 存放到标准输出流的缓冲区

scanf的作用是将(从)标准输入(键盘)(获取)的数据 放到变量中,其实就是在标准输入流的缓冲区中获取数据

printf和scanf,fprintf和fscanf其实本质上是差不多的,都是对流的操作 只不过printf和scanf是对标准输入输出操作 fprintf和fscanf是可以指定流

接下来学习的fprintf和fscanf都可以像printf和scanf这么理解 只不过流可以我们自己指定


    刚才学习的fgetc()/fputc()/fgets()/fputs()以及相关的函数都是将数据作为字符类型进行操作,但是如果我们想将数字相关类型(int,float类型等)读/写文件显然是不可以的。

例如我们想在某文件中写入float类型3.5,则不能使用fputc()/fputs()。这时我们可以使用我们熟悉的printf()/scanf()函数以及它们的同族函数fprintf()/fscanf()实现数据的格式化读/写。

fprintf和fscanf也是操作流的,比如:

int fprintf(FILE *fp,const char *format,...);是将 buf的数据打印到流FILE *fp中 其实就是将 buf的数据放到流fp中 

int fscanf(FILE *fp,const char *format,...);是将流FILE *fp中的数据放到buf中fscanf遇到空格和换行时结束,注意空格时也结束。

函数scanf()、fscanf()、sscanf()

    需要头文件:#include<stdio.h>

    函数原型:int scanf(const char *format,...);

              int fscanf(FILE *fp,const char *format,...);

              int sscanf(char *buf,const char *format,...);

    函数参数:format:输入的格式

                             fp:待操作的流

                           buf:待输入的缓冲区(不是流缓冲区 不要搞混了)

    函数返回值:成功:读到的数据个数

                        失败:EOF

fscanf遇到空格和换行时结束,注意空格时也结束。

函数printf()、fprintf()、sprintf()

    需要头文件:#include<stdio.h>

    函数原型:int printf(const char *format,...);

              int fprintf(FILE *fp,const char *format,...);

              int sprintf(char *buf,const char *format,...);

    函数参数:format:输出的格式

                            fp:待输出的流

                          buf:待输出的缓冲区(不是流缓冲区 不要搞混了)

    函数返回值:成功:输出的字符数

                        失败:EOF

示例:使用fprintf()函数向文件中写入一些数据,然后使用fscanf()函数读出这些数据
 

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp1,*fp2;
 
    if((fp1=fopen("scanftest.txt","w+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fprintf(fp1,"%d %d %d %d %s",1,2,3,4,"HelloWorld\n");
 
    fclose(fp1);
 
    /*文件写入数据完毕*/
 
    /*开始读取文件数据*/
 
    int a,b,c,d;
 
    char str[32];
 
    if((fp2=fopen("scanftest.txt","r+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fscanf(fp2,"%d%d%d%d%s",&a,&b,&c,&d,str);
 
    printf("a is %d\nb is %d\nc is %d\nd is %d\nstr:%s\n",a,b,c,d,str);
 
    fclose(fp2);
 
    return 0;
 
}
 

/****************关于sscanf()与sprintf()****************/

sscanf()与sprintf()函数的第一个参数都是字符型指针。sscanf()函数可以在字符串中读出指定格式的数据,sprintf()函数可以将数据按指定格式写入到某字符数组中。

示例:使用sscanf()函数在一个字符串中读出指定的数据
 

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    int a,b,c,d;
 
    if((fp=fopen("scanftest.txt","w+b"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    sscanf(argv[1],"%d.%d.%d.%d",&a,&b,&c,&d);
 
    printf("a is %d\nb is %d\nc is %d\nd is %d\n",a,b,c,d);
 
    fclose(fp);
 
    return 0;
 
}

编译后执行a.out 192.168.1.20,则可将ip地址(字符串)内的数据读出并写入变量a,b,c,d(int型)中。

/****************关于sscanf()与sprintf()end*************/


7、指定大小读/写文件


格式化输入/输出函数fscanf()/fprintf()使用比较方便,程序也简单易懂,但是fscanf()/fprintf()的读/写效率低下。一般在程序开发过程中,更多的使用fread()/fwrite()函数来一次读/写几个数据。

    函数fread()

    需要头文件:#include<stdio.h>

    函数原型:size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

    函数参数:ptr:存放读入数据的缓冲区(不是流缓冲区 不要搞混了)

                   size:读取的每个数据项的大小(单位字节)

             nmemb:读取的数据个数

              stream:要读取的流

    函数返回值:成功:实际读到的nmemb数目

                        失败:0

函数fwrite()

    需要头文件:#include<stdio.h>

    函数原型:size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream);

    函数参数:ptr:存放写入数据的缓冲区(不是流缓冲区 不要搞混了)

              size:写入的每个数据项的大小(单位字节)

              nmemb:写入的数据个数

              stream:要写入的流

    函数返回值:成功:实际写入的nmemb数目

                        失败:0

注意:

    ⒈fread()函数和fwrite()函数会将流当做二进制流的形式进行读/写,因此使用fread()/fwrite()操作的文件使用vim打开可能会出现乱码情况。

    ⒉fread()函数结束时,无法自动判断导致fread()函数结束的原因是读取到了文件末尾还是发生了读写错误。这时需要手动判断发生的情况。可以观察最后一次fread()的返回值,或使用feof()/ferror()函数判断。

示例1:使用fread()函数一次性读取1000个字节的数据

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    char buf[1000];
 
    int i;
 
    if((fp=fopen(argv[1],"r+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fread(buf,sizeof(char),1000,fp);//buf:读取后存放的位置;sizeof(char):每次读的大小;1000读的次数;fp:从什么地方读
 
    for(i=0;i<1000;i++)
 
    {
 
        putchar(buf[i]);
 
    }
 
    fclose(fp);
 
    return 0;
 
}

示例2:将一个int型数组{0,1,2,3,4,5,6,7,8,9}写入一个文件中

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    int a[10]={1,2,3,4,5,6,7,8,9,10};
 
    int i;
 
    if((fp=fopen(argv[1],"w+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
   fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);
//a:要写入的数据存放的位置;sizeof(a[]):每次写的大小;sizeof(a)/sizeof(a[0])写的次数;fp:写到什么地方去
 
    fclose(fp);
 
    return 0;
 
}

不过查看该文件会发现是乱码

练习1:读以下程序,猜想该程序会向文件中输入什么数据,运行程序证明猜想

#include<stdio.h>
 
#include<stdlib.h>
 
int main(int argc, const char *argv[])
 
{
 
    if(argc<2)
 
    {
 
        printf("too few arguments\n");
 
        exit(0);
 
    }
 
    FILE *fp;
 
    int a[]={1632397644,1768444783,1852139610,1635084371,663913};
 
    if((fp=fopen(argv[1],"w+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);
 
    fclose(fp);
 
    return 0;
 
}

练习2:使用标准I/O的fread()/fwrite()函数实现文件的复制

提示:分别打开两个文件,一个源文件一个目标文件,循环从源文件中使用fread()取出数据,然后使用fwrite()函数写入目标文件中。注意fread()函数的循环结束条件。

#include<stdio.h>
 
#include<stdlib.h>
 
#define MAX 128
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp1,*fp2;
 
    char buf[MAX];
 
    int n;
 
    if(argc<3)
 
    {
 
        printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);
 
        exit(0);
 
    }
 
    if((fp1=fopen(argv[1],"r"))==NULL)
 
    {
 
        perror("cannot open file1");
 
        fclose(fp1);
 
        exit(0);
 
    }
 
    if((fp2=fopen(argv[2],"w"))==NULL)
 
    {
 
        perror("cannot open file2");
 
        fclose(fp2);
 
        exit(0);
 
    }
 
    while((n=fread(buf,sizeof(buf[0]),sizeof(buf),fp1))>0)
 
    {
 
        fwrite(buf,sizeof(buf[0]),n,fp2);
 
    }
    
 
    fclose(fp1);
 
    fclose(fp2);
 
    return 0;
 
}

   while((n=fread(buf,sizeof(buf[0]),sizeof(buf),fp1))>0)
    {
        fwrite(buf,sizeof(buf[0]),n,fp2);
    }
    上面代码的通过这个循环实现文件的复制 是因为每读取sizeof(buf)个字节的数据到buf缓冲区

文件偏移量就会后移sizeof(buf) 所以可以通过多次循环直到fread返回0 读取完整个文件的数据并写入到另外一个文件

8、流的定位


    每次使用流打开文件并对文件进行操作后,都会让操作文件数据的位置发生偏移。在打开流的时候,偏移位置为0(即文件开头)随着读写的进行,偏移位置会不断向后,每次偏移量自动增加实际读写的大小。(这和文件IO打开文件和读写文件的规则是一样的 )可以使用fseek()函数和ftell()函数对当前流的偏移量进行定位操作

我们在前面学习文件IO时知道 函数lseek() 的功能是改变当前文件偏移量 且其返回值是 返回结果偏移量 所以我们可以使用 n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小

标准IO将文件IO中的 lseek() 的1.改变文件偏移量 2.返回当前文件偏移量 功能 分别定义成了两个函数 1.fseek 2.ftell

函数fseek()

    需要头文件:#include<stdio.h>

    函数原型:int fseek(FILE *stream,long offset,int whence);

    函数参数:stream:要定位的流

                       offset:相对于基准点whence的偏移量,正数表示向前(向文件尾方向)移动即向后偏移,负数表示向后(向文件头方向)移动即向前偏移,0表示不移动

                    whence:基准点(取值见下)

    函数返回值:成功:0,改变读写位置

                        失败:EOF,不改变读写位置

    其中第三个参数whence的取值如下:

        SEEK_SET:代表文件起始位置,数字表示为0

        SEEK_CUR:代表文件当前的读写位置,数字表示为1

        SEEK_END:代表文件结束位置,数字表示为2

    使用fseek()函数可以定位流的读写位置,通过偏移量+基准值的计算将读写位置移动到指定位置,其中第二个参数offset的值为正时表示向后移动,为负时表示向前移动,0表示不动。
 

示例:读取一个文件的最后10个字节

#include<stdio.h>
 
#include<stdlib.h>
 
#define MAX 10
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    char buf[MAX];
 
    int n;
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n");
 
        exit(0);
 
    }
 
    if((fp=fopen(argv[1],"r"))==NULL)
 
    {
 
        perror("cannot open file1");
 
        exit(0);
 
    }
 
    fseek(fp,-10,SEEK_END);
 
    fread(buf,sizeof(buf[0]),MAX,fp);
 
    for(n=0;n<MAX;n++)
 
    {
 
        putchar(buf[n]);
 
    }
 
    fclose(fp);
 
    return 0;
 
}

注意实际输出只能输出9个字符,因为最后一个字符为'\n'。

若想知道当前的读写位置的偏移量,则可以使用ftell()函数

函数ftell()

    需要头文件:#include<stdio.h>

    函数原型:int ftell(FILE *stream);

    函数参数:stream:要定位的流

    函数返回值:成功:返回当前的读写位置

                        失败:EOF


示例:在上一个示例程序中添加ftell()函数

#include<stdio.h>
 
#include<stdlib.h>
 
#define MAX 10
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    char buf[MAX];
 
    int n;
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n");
 
        exit(0);
 
    }
 
    if((fp=fopen(argv[1],"r"))==NULL)
 
    {
 
        perror("cannot open file1");
 
        exit(0);
 
    }
 
    fseek(fp,-10,SEEK_END);
 
    printf("This location is %ld\n",ftell(fp));
 
    fread(buf,sizeof(buf[0]),MAX,fp);
 
    for(n=0;n<MAX;n++)
 
    {
 
        putchar(buf[n]);
 
    }
 
    fclose(fp);
 
    return 0;
 
}

练习:使用fseek()函数和ftell()函数求一个文件的大小。

提示:先使用fseek()定位到文件末尾,再使用ftell()得到值。

#include<stdio.h>
 
#include<stdlib.h>
 
#define MAX 10
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n");
 
        exit(0);
 
    }
 
    if((fp=fopen(argv[1],"r"))==NULL)
 
    {
 
        perror("cannot open file1");
 
        exit(0);
 
    }
 
    fseek(fp,0,SEEK_END);
 
    printf("File is %ld\n",ftell(fp));
 
    fclose(fp);
 
    return 0;
 
}

 可以使用ls -l指令与该程序的执行结果进行对比。


9、其他常用标准I/O操作函数


①刷新缓冲区fflush()

函数fflush()

    需要头文件:#include<stdio.h>

    函数原型:int fflush(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:成功:0

                        失败:EOF

    fflush()函数会清除(原意是“冲刷”)该流内的输出缓冲区并立即将输出缓冲区的数据写回,即强迫将缓冲区内的数据写回流stream指定的文件中。若fflush()的参数为0(或NULL)则会刷新所有已经打开的流的输出缓冲区。

注意:

    ⒈fflush()函数可能会执行失败,当fflush()函数执行失败时会返回EOF,这可能由于缓冲区数据意外丢失或其他未知原因。因此当某些重要文件需要设置时,若使用fflush()刷新缓冲区失败,则应考虑使用文件I/O相关操作(open()、close()、read()、write()等)来代替标准I/O操作。

    ⒉!!!请不要试图使用fflush()刷新stdin!!!

    在C标准和POSIX标准中,fflush()仅定义了刷新输出流的行为,对于输入流stdin的fflush()操作是“未定义”(undefined),因此不同操作系统不同编译器对fflush(stdin)的操作都不会相同。fflush(stdin)操作只对部分编译器(例如VC++)等有效,而现在的大多数编译器(gcc等)是无效的。按C99标准规定的fflush()函数定义来说,fflush()函数是不允许刷新stdin的。

(原文:For input streams, fflush() discards any buffered data that has been fetched from the underlying file, but has not been consumed by the application. 大意是说如果对fflush传入一个输入流,会清除已经从输入流中取出但还没有交给程序的数据,而对输入流中剩余的未被程序处理的数据没有提及,可能不受影响,也可能直接丢弃)

对于fflush(stdin)操作,该操作未被标准定义,行为不确定,不同系统不同编译器操作不相同(可移植性不好),因此极为不推荐使用fflush()刷新标准输入流stdin。

②判断文件是否已经结束feof()

函数feof()

    需要头文件:#include<stdio.h>

    函数原型:int feof(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:文件结束:非0的值

                     文件未结束:0

    feof()函数可以检测流上的文件结束符,若文件结束则返回一个非0值,否则返回0。

    文件结束符EOF的数值是0xFF(十进制为-1),在文本文件中我们可以通过fgetc()是否读取到EOF来判断文件是否结尾(见fgetc()的示例程序)。而在二进制文件中可以有数值-1,因此就无法使用fgetc()读取EOF的方法来判断文件是否结尾。这时我们可以使用feof()函数来判断,feof()函数既可以判断文本文件又可以判断二进制文件。

feof()的常用用法有

if(feof(stream))//判断该流是否已结尾

或者

while(!feof(stream))//循环操作该流直至文件结尾

等。

③回到文件开头rewind()

函数rewind()

    需要头文件:#include<stdio.h>

    函数原型:void rewind(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:无

rewind()函数会将当前读写位置返回至文件开头(rewind原意为“(磁带等)回滚,倒带”),其等价于

(void)fseek(stream, 0L, SEEK_SET)


综合练习:循环记录系统的时间

每过1s,读取一次当前系统时间,之后写入到文件中。再次操作该文件不会删除文件内的原始数据而是继续向后书写数据。

提示:打开文件--->获取系统时间--->写入文件--->延时1s

                       ↑                        ↓

                 ----------死循环---------

答案:
 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<time.h>
 
#define MAX 64
 
int main(int argc, const char *argv[])
 
{
 
    FILE *fp;
 
    char buf[MAX];
 
    int n;
 
    time_t t;
 
    if(argc<2)
 
    {
 
        printf("arguments are too few\n");
 
        exit(0);
 
    }
 
    if((fp=fopen(argv[1],"a+"))==NULL)
 
    {
 
        perror("cannot open file");
 
        exit(0);
 
    }
 
    while(1)
 
    {
 
        t = time(NULL);
 
        fprintf(fp,"%s",asctime(localtime(&t)));
 
        fflush(NULL);//刷新缓冲区
 
        printf("%s",asctime(localtime(&t)));
 
        sleep(1);
 
    }
 
    fclose(fp);
 
    return 0;
 
}


***************

一道关于fork和printf的题_cy295957410的博客-CSDN博客一道关于fork和printf的题以下有4段代码,问分别输出什么#include<stdio.h>#include<unistd.h>void main(){int i=0;for(;i<2;i++){printf("a");fork();}}我的答案:8个a。实际答案:8个a#include<stdio.h>#include<unistd.h>void main(){int i=0;for(;ihttps://blog.csdn/cy295957410/article/details/112693807fork和printf问题_qq_33436509的博客-CSDN博客_fork printf一,fork函数#include &lt;unistd.h&gt;pid_t fork(void)返回值:-1,失败。子进程返回0,父进程返回子进程的进程ID。父子进程的0~3G用户地址空间,差不多一样。比如数据段,代码段,栈,堆,环境变量、用户ID、宿主目录、进程工作目录、信号处理方式,缓冲区等。父子进程不一样的是:进程ID,父进程ID,fork返回值,进程运行时间,未决信号集,...https://blog.csdn/qq_33436509/article/details/82385108
printf和fwrite都是库函数,而write则是系统直接调用的:结合已有知识,我们了解到当使用库函数命令时,打印消息并没有直接写到输出位置上,而是先把数据写到输出缓冲区,在刷新至输出位置。

    1、当输出目标位置为输出到显示器时,则刷新方式是行刷新;

    2、当输出目标位置为输出到文件中时,刷新方式由行缓冲变为全缓冲,全缓冲是指当把缓冲区写满后才能刷新。(或者强制刷新)代码中printf和fwrite第一次打印在fork操作之前,第二次打印则在fork操作之后,原因是在fork操作前,printf和fwrite的输出命令将数据先写到缓冲区中,此时只执行了这两条命令,由于是全缓冲的刷新方式,所以这两条命令并不足以将缓存写满,所以数据暂存在缓冲区中;然后进行fork创建子进程,由于fork创建出的父子进程代码共享,而数据不共享,各自私有一份,缓冲区中的数据都属于数据,因为父进程的残留数据还在缓冲区中,所以fork完毕后,父子进程将缓存中的数据各自存一份由父进程残留数据的数据,所以当父子进程各自刷新时,父子进程会各自执行一次printf和fwrite的输出命令,所以命令就从原来的两条变为四条。
 


fopen和open的使用场景

最主要的是:

open是系统函数,不可移植

 fopen是标准C函数,可移植

本文标签: 文件LinuxIO