Linux学习:文件IO(不带缓冲区),原子操作概念

lvan_linux

在前面的文章我们介绍过不带缓冲区的IO,这节我们主要介绍不带缓冲区的IO相关内容。原子操作对于文件共享是十分重要的,因此我们将介绍一些原子操作相关概念。

1:文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用。当我们打开或者创建一个文件的时候,都会返回一个非负的整数,我们打开和创建文件都用这个非负的整数。
同时,通常在我们执行一个进程的时候,都会默认打开三个文件描述符,他们分别是标准输入(0:STDIN_FILENO)、输出(1:STDOUT_FILENO)、错误(2:STDERR_FILENO)相关联的。他们的宏定义在unistd.h文件中。
文件描述符上限是OPEN_MAX。

2:open函数介绍


函数原型:int open(const char* pathname, int flag);
                                         int open(const char* pathname, int flag, mode_t mode);
pathname:是打开文件的路径名。
flag:是打开文件的一些选项。
mode:只有我们需要创建文件的时候,才会将指定mode的模式。

O_RDONLY:只以读的方式打开文件。
O_WRONLY:只一写的方式打开文件。
O_EXCL:测试文件受否存在,如果不存在则创建文件。(注:如果同时指定O_CREAT并且文件已经存在则出错)
O_APPEND:每次写入都追加到文件末尾。
O_CREAT:如果文件不存在,则创建文件。使用这个选项的时候,需要第三个参数。
        mode:权限位,也就是我们使用ls -l显示的各种读写和执行权限的位。
        S_IRWXU:用户有读写执行权限。
        S_IRUSR:用户有读权限。
        S_IWUSR:用户有写权限。
        S_IXUSR:用户有执行权限。
        S_IRWXG:同组用户有读写执行权限。
        S_IRGRP:同组用户有读权限。
        S_IWGRP:同组用户有写权限。
        S_IXGRP:同组用户有执行权限。
        S_IRWXO:其他用户有读写执行权限。
        S_IROTH:其他用户有读权限。
        S_IWOTH:其他用户有写权限。
        S_IXOTH:其他用户有执行权限。
O_TRUNC:如果文件存在的话,并且我们只为读写打开文件的话,那么文件长度将会截断为0。也就是文件长度会变为0,相当于我们将文件内容全部删去再重新写入。
O_NOBLOCK:指定文件为非阻塞IO,正常情况下在我们写入文件的时候,我们都等待写入完成之后再返回。如果指定此flag,那么将直接返回,而不管是否写入完成。
O_SYNC:每一次写操作完成之前都会更新文件的属性。
O_RSYNC:等待任何对文件同一部分未决写操作完成。(这个标志和下面一个标志不是很理解,如果理解的话,请在评论区讨论。最好是能有一个实际的例子)。
O_DSYNC:每次写等待物理操作完成。仅当文件属性根棍以反映文件数据变化时,此标志才会影响文件属性。

open函数返回的文件描述符一定是当前可用的最小文件描述符。

文件名和路径名的截断:
这个概念我简单介绍一下,就是我们输入打开文件的路径名的时候,由于我们输入的太长了,我们无法识别那么长的路径名,就会保留其中的一部分。这样就无法识别出具体
的打开的文件了(我们保存这部分路径名可能与其他文件的路径名重名)。
我们这里可以使用pathconf获取一些路径名长度,我发现我的机器长度限制是4096。


creat:创建文件
函数原型:create(const char *pahtname, mode_t mode);
此函数和open函数是有雷同的部分,不做了解。

close:关闭打开的文件描述符(就是我们打开一个文件一定会用到一些资源去管理这个打开的文件,那么这里关闭就是释放这些被栈用的资源)
函数原型:int close(int filedes);

lseek函数:更改当前文件的偏移量,也就是我们从文件哪里开始读写文件。
 当我们打开一个文件或者创建一个新的文件,文件偏移量默认为0。也就是说从文件开始位置读写文件。同时我们也可以用lseek函数将当前文件偏移量定位到文件指定的置为。
 函数原型:lseek(int filedes, off_t offset, int whence);
                                            lseek_64(int filedes, off64_t offset, int whence);
 filedes:是文件描述符。
 offset:是偏移的大小。(可以为正<增加文件偏移>,也可以为负<减少文件偏移>)
 关于off_t是否够用的问题,我在这里简单讲解一下。 但是我们看函数原型的时候,有32位和64位两种文件偏移量。如果是32位那么文件偏移量最大时2的32次方减一,64位也是如此。
 32位的文件偏移量最大时2TB(2^31-1字节),64位文件最大偏移量2^33TB,应该足够使用了。如果是不同的平台会用不同的大小,这里只列举了32位和64位。具体实现还是靠硬件,
 这里就不逐一列举了。
 whence:是指定位置开始进行便宜。
             SEEK_SET:即从文件开头为只进行便宜。
             SEEK_CUR:从当前文件位置进行便宜。
             SEEK_END:从文件末尾位置开始便宜。
同时lseek也可以测试文件是否设置偏移量,如果文件描述符引用的是一个管道、套接字,则lseek返回-1.


read函数:从文件中读取指定字节数目的数据。
函数原型:ssize_t read(int filedes, void *buf, size_t nbytes);
filedes:文件描述符。
buf:读取到字节存放的缓冲区。
nbytes:读取多少个字节。
返回值:实际读取到的字节数。
实际读取到字节数可能小于我们要求读取的字节数。(eg,当前文件位置到文件末尾还有三十个可读的字节,但是我们要求读取200个字节,这就返回30个字节。但是在下次读取的时候,
返回30个字节。还有从网络中读取数据,有些时候可能由于网络延迟读取小于我们读取到的字节数)。


write函数:向文件中写入指定字节数的数据。
函数原型:ssize_t write(int filedes, const void* buf, size_t nbytes);
同上,buf是写入字节存储位置。nbytes是写入的字节数。返回值是实际写入的字节数。
同时,如果我们指定了O_APPEND,每次我们写入之前都将文件偏移量设置为文件末尾的位置。

文件共享:
在介绍文件共享之前,我们先介绍一下内核是使用什么结构表示打开的文件的。
内核使用三种数据结构表示打开的文件。
(1)每个进程都有一个记录项,记录项抱哈一张打开的文件描述符表,每个文件描述符占用一项。我们在进程中使用一个整型记录这个文件项的位置。
        每个文件描述符相关联的有两部分:文件标志,指向文件表的指针。
(2)对于打开的文件内核维持一张文件表。
    文件表中有文件状态标志、当前文件的偏移量和v节点指针。
(3)每个打开的文件都有一个v节点。
    v点包含了文件类型和对该文件各种操作的函数指针。同时还有i节点信息,当前文件的长度。
关于这些内容了解即可。如下图所示,可以很好的理解这三者之间的关系。

file


同时,当我们两个进程同时打开一个文件的时候,其结构是下图这样的。

file


根据上述内容可以理解,如果是多个进程同时向一个文件中写入数据的时候,可能产生预期不到的结果。如果想要避免这种问题,就得理解原子操作相关的概念。

原子操作:
        概念:不受其他影响的独立完成一个操作的全过程。如下程序,我们向一个文件尾部添加内容。
        lseek(fd, SEEK_END, 0);
        write(fd, buf, size);
        如果是一个程序,那么我们这样写没什么问题。但是如果是两个独立的进程,那么这样写可能就麻烦了(如两个进程分别是进程A和进程B,
        如果进程A需要写100字节,进程B需要写入1000字节。那么可能在进程A写入50字节的时候进程B开始写入,并且进程B写入1000字节。那么这是进程A写入就会覆盖
        进程B写入开头的50字节,这样记录的信息可能就出现了损坏)。如下图所示,两个文件向一个文件写入内容。
        

file

dup和dup2函数:复制文件描述符。
函数原型: int dup(int filedes);
                                            int dup2(int filedes, int filedes2);
前一个函数返回一个新的文件描述符,这个新的文件描述符指向的文件表项和filedes文件描述符指向的文件表项是同一个(返回的文件描述符一定是当前进程表项中最小的文件描述符)。
第二个函数使用指定的文件描述符去复制filedes文件描述符,如果filedes2已经使用,则先关闭filedes2,然后在复制filedes。
参考下述图片,去理解复制一个文件描述符的意义。

file

前面曾经提到过IO缓冲区,当我们使用缓冲区进行向磁盘写入数据的时候,有些事后我们可能无法及时将数据写入到磁盘中。当我们的机器意外出现事故的时候,我们可能丢失数据。
还有就是一些数据库需要及时的保存,因此、我们需要让写这个操作及时进行,下面有三个函数会将数据及时写入到磁盘中。
函数原型:int fsync(int filedes);将指定文件描述符数据写入到磁盘中,这里是等待写操作结束返回。
                                         int sync();将所有修改过的缓冲区排入写操作队列。
                                         int fdatasync(int filedes);将数据写入到磁盘的同时还将文件属性更新。
                                         
前面曾经文件的一些性质,这里呢有个函数可以通过文件描述符改变打开文件的一些性质。
函数原型:int fcntl(int filedes, int cmd, ...);
                (1):复制现有的描述符。
                (2):获得/设置文件描述符标志。
                (3):获得/设置文件状态标志。
                (4):获得/设置异步I/O所有权(异步IO是通过信号的方式实现的,在讲解套接字部分会有所讲解)。
                (5):获得/设置记录所
参看man手册,我们会发现如下许多cmd。
        F_DUPFD:复制一个新的文件描述符。
        
        F_GETFD/F_SETFD:获得和设置文件描述符标记。当前的标记只有一种,就是FD_CLOEXEC(在执行exec函数的时候关闭该文件描述符)。
        
        F_GETFL/F_SETFL:获得和设置文件状态标记。文件状态标记在前面除了O_DSYNC和O_SYNC都可以改变。(eg:读,写,追加等等)我这里参考的是我这个版本的man手册,
        如果想要了解请参考你的版本man手册。
        
        F_GETOWN/F_SETOWN:获得和设置异步IO所有权。在学习套接字部分将会了解到这里。
        
        关于记录锁,使用如下图的结构。
        F_SETLK/F_SETLKW/F_GETLK:请求或者释放一个锁/设置一个文件锁/返回一个锁(如果没有锁,那么在l_type字段设置为F_UNLCK)。
        在学习多进程的时候,我会写一个实验关于文件锁的,就是多个进程读写文件,使用文件的记录锁实现文件共享。

![file](/img/bVcNPlq)




    
 
阅读 168
4 声望
0 粉丝
0 条评论
你知道吗?

4 声望
0 粉丝
宣传栏