Linux下文件的深入了解(下篇)---学习笔记三

74 阅读11分钟
  1. 复制文件描述符
  • 1.1 dup()函数
  • 1.2 dup2()函数
  1. 文件共享 -2.1 通过open函数多次打开同一份文件实现文件共享 -2.2 不同进程多次打开同一份文件实现文件共享 -2.3 同一个文件通过dup、dup(2)函数实现文件共享

  2. 竞争冒险与原子操作

  • 3.1 竞争冒险
  • 3.2 原子操作
  1. fcntl函数和ioctl函数
  • 4.1 fcntl函数
  • 4.2 ioctl函数
  1. 截断文件

1 复制文件描述符

通过open函数获取的文件描述符,可以进行复制得到有一个新的文件描述符,同样也是可以用新的文件描述符对文件进行IO操作,且复制的文件描述符与被复制的文件描述符拥有相同对文件的操作权限。因为新的文件描述符和旧的描述符指向同一文件表,也就意味着新的文件描述符的文件描述符属性与被复制的文件描述符属性是一样的,例如文章偏移量、文件权限、inode指针、文件状态标志等。 下列是两种复制文件描述符的函数操作方法

1.1 dup()函数

dup用于复制文件描述符,函数原型为dup(int oldfd),使用此函数必须包含<unistd.h>头文件。

oldfd:为输入参数,被复制的整形文件描述符;

返回值:成功返回复制的新文件描述符,失败则返回-1。

示例:创建一个新文件,文件描述符为fd1,通过dup函数进行文件描述复制,得到新的文件描述符fd2,分别通过文件描述符fd1和fd2轮流写入一个字节,一共写入4次,打印输出两个文件描述符,打印输出文件内容。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd1,fd2;
    char buf1[4],buf2[4],buf[8];
    inr ret,i;
    
    fd1 = open("./home/ccc.c", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(fd1 == -1)
    {
        perror(fd1OpenError);
        exit(-1);
    }
    
    fd2 = dup(fd1);
    if(fd2 == -1)
    {
        perror(fd2DupError);
        exit(-1);
    }
    
    printf("fd1=%d,fd2=%d\r\n",fd1,fd2);
    
    memset(buf1,0x55,sizeof(buf1));
    memset(buf2,0xAA,sizeof(buf2));
    memset(buf,0x00,sizeof(buf));
    
    for(i = 0; i<4; i++)
    {
        ret = write(fd1,buf1,1);
        if(ret == -1)
        {
            perror(fd1WriteError);
            exit(-1);
        }
        
        ret = write(fd2,buf2,1);
        if(ret == -1)
        {
            perror(fd2WriteError);
            exit(-1);
        }
    }
    
    ret = lseek(fd1, 0, SEEK_SET);
    if(ret == -1)
    {
        perror(lseekError);
        exit(-1);
    }   
    
    ret = read(fd1, buf, 8);
    if(ret == -1)
    {
        perror(ReadError);
        exit(-1);
    }
    for(i=0; i<8; i++)
    {
        print("0X%x",buf[i]);
    }
    
    close(fd1);
    close(fd2);
    return 0;
}

上述程序在打印后输出为交替输出,说明通过复制文件描述符可以实现接续写。

1.2 dup2()函数

dup2也是用于复制文件描述符,可以手动指定文件描述符,不需要遵循系统文件描述符分配原则,函数原型为dup(int oldfd,int newfd),使用此函数必须包含<unistd.h>头文件。

oldfd:输入参数,被复制的整形文件描述符;

newfd:输入参数,复制后指定的文件描述符;

返回值:成功返回复制的新文件描述符,失败则返回-1。

示例:打开一个已存在的文件,复制一个为50的文件描述符并打印输出复制前后的文件描述符。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd1,fd2;
    fd1 = open("./home/ccc.c",O_RDWR);
    if(fd1 == -1)
    {
        perror(fd1OpenError);
        exit(-1);
    }
    
    fd2 = dup2(fd1,50);
    if(fd2 == -1)
    {
        perror(fd2Dup2Error);
        exit(-1);
    }
    
    printf("fd1=%d,fd2=%d",fd1,fd2);
    
    close(fd1);
    close(fd2);
    return 0;
}

2 文件共享

文件共享即通过多次打开同一个文件或通过dup、dup2函数获取到到多个文件描述符,多个文件描述符都对同一个文件同时进行IO操作,即通过一个文件描述符去IO读写操作完后未调用close去关闭,同时通过另一个文件描述符去IO读写操作文件。

2.1 通过open函数多次打开同一份文件实现文件共享

同一进程通过open函数多次打开同一份文件,获得多个不同的文件描述符对应不同的文件表,所有的文件表都索引到了同一个inode节点,也就是磁盘上的同一个文件。

3.png

2.2 不同进程多次打开同一份文件实现文件共享

与2.1差别在于不是同一个进程内通过open函数进程打开同一份文件,即两个独立的程序同时去打开同一份文件,那么程序一的文件描述表的fd1指向程序一的文件表,程序二的文件描述表的fd2指向程序二的文件表,最后都索引到了同一个inode节点,从而实现程序进程间实现文件共享。

4.png

2.3 同一个文件通过dup、dup(2)函数实现文件共享

同一个进程内通过dup、dup2函数去复制多个文件描述,得到的不同文件描述符都指向同一份文件表,因此也是同一个inode节点,操作磁盘同一份的文件。

5.png

3 竞争冒险与原子操作

3.1 竞争冒险

什么是竞争冒险? 例如当两个进程同时对同一份文件进行IO操作时,且都未使用 O_APPEND 标志,进程一通过lseek函数将位置偏移量移动到距离文件头1500字节处,此时进程一时间片耗完,开始进行进程二的时间片,进程二也将位置偏移量移动到距离文件头1500字节处,然后通过write函数写入100字节,进程二时间片用完,进程一继续写入100字节,那么进程二在1500到1600写入的数据将被经常二覆盖。进程一和进程二工作流程如下所示例。

image.png

因此上述进程一和进程二这一情况就成为了一种竞争冒险。两个经常操作同一个文件资源,但CPU使用先后是不可预期的,由操作系统调配,这就是竞争状态。

3.2 原子操作

既然存在竞争状态,那要如何消除呢?那就是原子操作了。上述出现这种情况主要是因为一个进程先移动到文件末尾,但时间片结束,此时另一进程移动到文件末尾,写入了数据,那么等再次回到上一进程时再写入数据,数据将会被覆盖掉。

因此,为了防止出现这种情况,必须要将移动位置偏移量和写数据的两个操作的步骤合成一个原子操作,原子操作即值多步骤执行的操作合并成一个操作。那么移动位置偏移量和写数据合成一个原子操作,这时候我们就可以想到使用O_APPEND标志。

通过O_APPEND标志实现原子操作,当open函数的flags参数中包含了O_APPEND标志时,每次使用write函数时都会将位置偏移量移动到末尾,然后再写入数据。不管其它进程先写入数据了,每次调用write时都会从末尾开始写入数据,这样就不会覆盖掉其它进程写入的数据了。

除了通过O_APPEND标志可以实现原子操作外,还可以通过pread()和pwrite()这两个函数来实现原子操作,这两个函数也是用于读取数据和写入数据,与read()和write()区别在于,pread()和pwrite()是先移动位置偏移量后接着读取数据和写入数据。将移动位置偏移量和读取和写入数据合并为一个原子操作。

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

pwrite函数原型:ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

输入参数:

fd:文件描述符

*buf:写入和读取的数据缓冲区的首地址

count:写入和读取的字节数

offset:表示当前需要进行读或写的位置偏移量

返回值:成功则返回当前读取或写入的字节数,失败则返回-1

注:使用pread()和pwrite()函数时,不会更新文件当前的偏移量,以下示例程序可以测试出这一结果。

示例:打开一个文件通过pread()在距离文件头1000字节处写入100字节,再通过lseek函数的SEEK_CUR标志来获取当前的位置偏移量。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd,ret;
    char buf[100];
    
    memset(buf,0x00,100);
    
    fd = open("./home/ggg.c",O_RDWR);
    if(fd == -1)
    {
        perror(OpenError);
        exit(-1);
    }
    
    ret = pread(fd,buf,100,1000);
    if(ret == -1)
    {
        perror(preadError);
        exit(-1);
    }
    
    ret = lseek(fd, 0, SEEK_CUR);
    if(ret == -1)
    {
        perror(lseekError);
        exit(-1);
    }
    
    printf("%d",ret);
    
    close(fd);
    return 0;
}

同理,在其它文件IO操作中也会存在冲突,例如在创建文件时,先是进程一使用open函数尝试去打开文件,结果返回失败;然后进程一时间片耗尽,进程二接着运行,也是使用open函数去尝试打开文件,结果也失败,接着进程二再次使用open函数但是加入了O_CREAT标志来创建此文件,open执行成功,文件创建成功,进程二时间片耗完后,返回进程一继续运行,进程一也使用了open函数加O_CREAT标志创建文件,函数执行成功。

7.png

故进程一和进程二都创建了一个相同文件名的文件,从而引起冲突,那么就需要将创建文件和判断文件是否存在合成一个原子操作,因此可以加入O_EXCL标志去判断文件是否存在,存在则返回错误,避免重复创建了同一文件。

4 fcntl函数和ioctl函数

4.1 fcntl函数

fcntl函数可以对一个已打开的文件描述符文件描述符执行一些命令操作。

fcntl()函数原型:int fcntl(int fd, int cmd, ... /* arg */ )

输入参数: fd:文件描述符 cmd:操作命令,即要对文件描述符fd进行的操作。

操作命令介绍: 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC); 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD); 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL); 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN); 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);

返回值:失败返回-1;执行成功返回值与执行的操作命令有关,例如cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符,cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

示例1:使用fcntl()函数复制一个文件描述符

当cmd=F_DUPFD时,会依据fd复制出一个新的文件描述符,此时需要传入第三个参数,第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符(没有使用的文件描述符);如果第三个参数等于一个已经存在的文件描述符,则取一个大于该参数的可用文件描述符。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd1,fd2;
    
    fd1 = open("./home/ggg.c",O_RDWR);
    if(fd1 == -1)
    {
        perror(OpenError);
        exit(-1);
    }
    
    fd2 = fcntl(fd1, F_DUPFD,0)
    if(fd2 == -1)
    {
        perror(fcntlError);
        exit(-1);
    }
    
    printf("fd:%d,fd2:%fd",fd1,fd2);
    
    close(fd1);
    close(fd2);
    return 0;
} 

示例2:使用fcntl()函数获取文件状态标志,并添加 O_APPEND 标志。

F_GETFL命令可用于获取文件状态标志,F_SETFL命令可用于设置文件状态标志。cmd=F_GETFL时,不需要传入第三个参数,返回值成功表示获取到的文件状态标志;cmd=F_SETFL时,需要传入第三个参数,此参数表示需要设置的文件状态标志。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd1,ret;
    
    fd1 = open("./home/ggg.c",O_RDWR);
    if(fd1 == -1)
    {
        perror(OpenError);
        exit(-1);
    }
    
    ret = fcntl(fd1, F_GETFL);
    if(ret == -1)
    {
        perror(GETFLError);
        exit(-1);
    }
    printf("GETFL=%d",ret);
    
    ret = fcntl(fd1, F_SETFL, ret | O_APPEND);
    if(ret == -1)
    {
        perror(SETFLError);
        exit(-1);
    }
    
    close(fd1);
    return 0;
} 

4.2 ioctl函数

ioctl函数一般用于操作特殊文件或硬件外设,例如可以通过 ioctl 获取 LCD 相关信息。

函数原型:int ioctl(int fd, unsigned long request, ...);

输入参数: fd:文件描述符

request:与具体要操作的对象有关,表示向文件描述符请求相应的操作

...:可变参数,由request决定

返回值:成功返回 0,失败返回-1

5 截断文件

通过使用truncate()或ftruncate()可以把文件截断指定大小字节。

truncate()函数原型:int truncate(const char *path, off_t length);

ftruncate()函数原型:int ftruncate(int fd, off_t length);

*path:文件路径

length:截断指定大小字节

fd:文件描述符

如果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节"\0"。

返回值:调用成功返回 0,失败将返回-1。

这两个函数不会导致位置偏移量的变化,不过一般截断后要重新设置偏移量,以为偏移量不存而导致出错,对于ftruncate()函数,必须要有open()函数打开该文件得到文件描述符,且必须要具有可写权限。

示例:分别使用这两函数截断两个文件长度为512个字节。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd,ret;
    
    fd = open("./home/ggg.c",O_RDWR);
    if(fd == -1)
    {
        perror(OpenError);
        exit(-1);
    }
    
    ret = ftruncate(fd, 512);
    if(ret == -1)
    {
        perror(ftruncateError);
        exit(-1);
    }
    
    
    ret = truncate("./home/aaa.c", 512);
    if(ret == -1)
    {
        perror(truncateError);
        exit(-1);
    }
    
    close(fd);
    return 0;
} 

最后可以通过ls -l 命令可以查看两个文件大小为512字节。