linux系统编程07-文件IO\系统调用IO

介绍

Untitled

文件描述符的概念

Untitled

  • 备用图

    Untitled

  1. 文件是一块磁盘空间,有一个编号 inode ,每次 open 一个文件时,会创建一个结构体,链接 inode ,存储文件的信息,结构体的首地址以指针的形式(类似于 FILE* )存放在一个数组中,向用户返回数组的下标,这就是文件描述符 fd 。因此,拿到下标就能拿到指针,拿到指针就能拿到结构体,从而操作文件。 结构体和数组是在 内存空间 中。 每个进程有各自的数组和结构体
  2. 由于标准IO是建立在系统调用IO的基础上的,因此 FILE 结构体中必然存在整型成员 fd
  3. 数组的大小是1024个,可以用 ulimit -a 查看,进程打开时, fd 0 1 2 对应 stream stdin stdout stderr,是默认打开的,所以 fd 从3开始
  4. 进程间的链接同一个 inode 的结构体互不影响,进程内链接同一个结构体的 fd 也互不影响。 结构体和 inode 都有 引用计数 。结构体的引用计数是用来操作打开文件的,记录有多少个 fdl链接向自己, inode 的引用计数是用来决定文件是否存在的(而不是记录有多少个结构体链接向自己):当结构体的引用计数为0时,结构体才会被销毁,所以不会出现把 fd4 close后, fd6 变成野指针的情况;当 inode 引用计数(初始化为1)为0时,文件会被销毁

软硬链接:

open\close

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

不是用重载,而使用变参实现

  • 参数:

    • pathname : 路径

    • flags :位图,用来控制打开方式、文件创建和文件状态

      O_READONLY O_WRONLYO_RDWR 是必选项

      Untitled

  • 返回值:成功返回文件描述符,失败返回-1

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

关闭一个文件描述符

read\write\lseek

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • read :从fd中读count个字节到buf
  • write :将buf中的count个字节写入fd中
  • 返回值:返回成功读入或写入的字节数,出错返回-1
  • lseek :用法同 fseek ,成功返回举例开头的字节数,失败返回-1 【等价于 fseek + ftell

Untitled

例子:实现 mycopy.c

示例代码:

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

##define BUFSIZE 1024
int main(int argc, char **argv)
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage:...\n");
        exit(1);
    }

    size_t sfd, dfd;
    int len=0, ret, pos;
    char buf[BUFSIZE];

    sfd = open(argv[1], O_RDONLY);
    if(sfd < 0)
    {
        perror("open()");
        exit(1);
    }
    dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600);
    if(dfd < 0)
    {
        perror("open()");
        exit(1);
    }

    while(1)
    {
        len = read(sfd, buf, BUFSIZE);
        if(len < 0)
        {
            perror("read()");
            break;
        }
        if(len == 0)
            break;

        pos = 0;
				//确保写够len个字节
        while(len > 0)
        {
            ret = write(dfd, buf+pos, len);
            if(ret < 0)
            {
                perror("write()");
                break;
            }
            pos += ret;
            len -= ret;
        }
    }

    close(dfd);
    close(sfd);

    exit(0);
}

运行结果:

Untitled

几点说明:

  • open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600); :只写打开,有则创建,无则删除,创建的权限为 0600
  • write 的循环是为了确保 写够len个字节,如果只写 if(ret<0) {perror(); break;} 会出现读入7byte,写入3byte但不报错的情况。场景:该进程被信号打断,没有写够;对设备进行io。

标准IO与系统调用IO的区别

举例:传达室老大爷跑邮局[拿一封送一封,一起送(缓冲区满)\加急时送(刷新缓冲区)]

区别:响应速度&吞吐量

面试:如何使一个程序变快

提醒:标准IO与系统调用IO不可混用

转换: fileno, fdopen

类型 响应时机 优势
标准IO 缓冲区满、刷新缓冲区 吞吐量大
系统调用IO 立刻执行 响应速度快

不可混用的原因:虽然FILE内部包含fd,并且FILE和fd可以通过两个函数转换,但是FILE内的属性如pos和fd指向的结构体内的属性往往是不同的,因为写的时候存在buf,读的时候存在cache。

比如:写10个byte,FILE的pos+10,但是此时缓冲区还没有刷新,所以fd指向的结构体内的pos不变

同理,读1个byte时,FILE的pos+1,但是由于cache存在,可能会预读取,所以fd指向的结构体的pos可能+10

Untitled

例子: 标准IO用系统调用IO实现,合并系统调用IO

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
    putchar('a');
    write(1, "b", 1); 

    putchar('a');
    write(1, "b", 1); 

    putchar('a');
    write(1, "b", 1); 

    exit(0);
}

运行结果:

Untitled

通过指令 strace ./ab 查看系统调用情况

Untitled

三次调用putchar(’a’)相当于底层调用一次 write(1,”aaa”,3)

其他内容

Untitled

Untitled

思路:类似于删除数组的中间元素。将11行后的内容一次搬到10行,知道搬完,最后减去10行的大小。truncate改变文件大小。

Untitled

通过两次打开同一个文件,简化系统调用,也可以用两个线程或进程

Untitled

dup\dup2

int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup :把 oldfd 文件描述符,复制到当前文件描述符数组的最低位。

dup2 :把 oldfd 复制到 newfd ,如果 newfd 已被占用,会把占用 newfd 的文件关闭,等价于 close()+dup() ,由于是原子操作所以可以避免并发竞争

例子:在程序中把标准输出重定向到一个文件中,并且输出

示例代码1:关闭 fd1 ,重新打开文件, fd 就会占用1

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

##define FNAME "/tmp/out"
int main()
{
    int fd; 

    close(1);

    fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);
    if(fd < 0)
    {   
        perror("open()");
        exit(1);
    }   

    /***************/
    puts("Hello");  

    exit(0);
}

示例代码2:用 dup 实现

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

##define FNAME "/tmp/out"
int main()
{
    int fd; 

    //close(1);

    fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);
    if(fd < 0)
    {   
        perror("open()");
        exit(1);
    }   
		
		close(1);
		dup(fd);
    /***************/
    puts("Hello");  

    exit(0);
}

示例代码3:用 dup2 实现,避免并发竞争

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

##define FNAME "/tmp/out"
int main()
{
    int fd; 

    //close(1);

    fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);
    if(fd < 0)
    {   
        perror("open()");
        exit(1);
    }   
		
		dup2(fd,1);
		if(fd != 1)
				close(fd);

    /***************/
    puts("Hello");  

    exit(0);
}

示例代码1、2都有并发问题:如代码2中, close(1) 之后,兄弟线程创建了一个文件,就占用了 fd1 ,所以 dup(fd) 占用的就再是 fd1 了。

示例代码3注意关闭 fd 内存泄漏:同时注意, if(fd != 1) 是为了防止一开始 fd1 就是空缺的,这样 fd=1 ,虽然 dup2(fd,1) 在oldfd==newfd的时候do nothing,如果不加判断,就是把 fd 关闭,导致后面的 puts 无法输出。

其他问题:程序结束后应该还原 fd1 到标准输出。

运行结果:

Untitled

要当自己在写一个模块,考虑前后文以及与其他模块的并发,而不仅仅是写一个main函数

文件同步

将 buffer 和 cache 中的内容同步到 disk(磁盘)

sync fsync fdatasync

fcntl\iocntl

int fcntl(int fd, int cmd, ... /* arg */ );

一切关于文件描述符的魔术,都来源于该函数。 管家级函数.

参数: cmd 指命令,不同的命令有不同的参数,返回值的含义也不同

ioctl :管家级别的函数。用来管理设备。【一切皆文件的理念的受害者,因为文件只有open\close\write\read\lseek五种基本操作,但是设备显然不仅仅如此,于是都交给 ioctl 来管, man ioctl_list

fcntl 设置对 fd 的IO为非阻塞:

Untitled

/dev/fd :虚目录,显示的是当前进程文件描述符的信息。

Untitled

相当于照镜子:谁全查看反映的就是谁,所以这里显示的是 ls 进程使用的文件描述符。

在程序中要得到当前程序所在进程使用的文件描述符,只需要在程序中打开该目录即可。

posted @ 2025-09-17 00:05  Miaops  阅读(20)  评论(0)    收藏  举报