Unix高级环境编程-记录笔记一(文件IO/系统IO)

一、标准IO

1、临时文件

在日常中,经常会产生临时文件。基于Linux多用户多任务系统下,可能会出现多个用户操作同一个临时文件的问题,造成覆写的情况。同时,一个用户可能产生临时文件的频率较高,几分钟一个等,但又没有及时清理的,那么在多个用户的情况下则造成产生的临时文件太多太大,一是会占用过多的内存,二就是会增加文件冲突的可能性。即多个用户操作同一个临时文件。

Linux定义了函数FILE* tmpfile(void);函数来帮助创建临时文件。它会创建一个临时的二进制文件(w+)类型为读写,在关闭2该文件或程序时将自动删除这个文件。可通过man手册查看:man tmpfile

二、系统IO

1、文件描述符的概念(贯穿系统IO始终)

实质上是一个int整型,是一个数组下标。整个过程是这样的:假设有一个文件inode,根据操作打开这个文件,打开这个文件后会得到什么呢?假设返回一个结构体,同标准IO一样,返回一个FILE*其中FILE是一个结构体,为什么读完一个文件中一个字节后会知道继续读下一个字节,猜想肯定是这个结构体中保存着一个指向文件字节的指针pos,把这个结构体的地址指针放到一个数组中,把这个数组的下标返回给open等IO操作函数。这些函数就可以通过这个下标访问数组中的保存的结构体的地址,从而拿到文件的内容。

当多次打开同一个文件时,会产生不同的结构体关联同一个文件,但是文件描述符不同,释放掉一个不影响另一个。

假设出现这种情况,把4位置的指针复制一份到6位置,那么4和6位置指针指向同一个结构体,此时释放4,这个结构体不会释放,这是Linux作了防护,所以猜想类似C++智能指针的情况,在这个结构体内会保存一个计数器count,记录指向这个结构体的指针个数。

在这里插入图片描述

这个数组有多大呢?可以通过ulimit -a命令查看,打开的最大文件数为1024。写程序验证时可能只会显示出1021个,因为系统默认打开stdin、stdout、stderr这三个默认标准IO。数组的前三个下标0、1、2就分别代表stdin、stdout、stderr。 这个数组存在进程空间中,每个进程都保存有这样一个数组。

在这里插入图片描述

由于标准IO是通过系统IO来实现的,猜测到FILE结构体中至少要有一个fd文件描述符来访问系统IO打开的文件。

2、文件IO操作:open、close、read、write、lseek

open函数:

返回类型为int,返回一个文件描述符,失败返回-1,并且返回一个errnum,这个errnum表示发生了何种错误。这个errnum也是一个整数,经过宏定义过的。

#include <fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int open(const char* pathname,int oflag,...);
int openat(int fd,const char* pathname,int oflag,...);

oflag参数说明此函数的多个选项,用下面的一个或多个常量进行按位|或的方式构成oflag参数。这五个常量必须指定一个且只能指定一个。

O_REONLY 只读打开
O_WRONLY  只写打开
O_RDWR  读写打开
O_EXEC  只执行打开
O_SEARCH 只搜索打开

这里的两个open函数采用的是变参形式的,而不是重载的方式。

判断两者的方法就是:调用它时给它传多个参数,如果编译报错,那么就是重载的方式,因为重载是定参的,不报错就是可变参数形式的,类似于printf()函数,可以传多个%d。

在这里插入图片描述

read函数:

从文件描述符fd处读,读到buf去,读取count个。返回值是读取到的字节数。如已到文件末尾,返回0

在这里插入图片描述

在这里插入图片描述

write函数:

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

size_t不带符号整型,ssize_t带符号整型。若成功,返回已写的字节数,失败返回-1。

lseek函数:

每个打开的文件都有一个与其关联的“当前文件偏移量”。通常是一个非负整数,计算从文件开始处到当前位置的字节数。读写操作都是从文件偏移量处开始的。

返回值:成功返回新的文件偏移量,失败返回-1

参数解释:

whenceSEEK_SET,则将文件的偏移量设置为距文件开始处offset个字节。

whenceSEEK_CUR,则将文件的偏移量设置为当前值加上offset,其中offset可为正或负。

whenceSEEK_END,则将文件的偏移量设置为文件长度加offset。其中offset可正可负。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09AfFB1i-1626848151213)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b0c0b8a0-be8a-4adf-a092-fb4b5319b6e8/Untitled.png)]

**实例:**使用open、read、close等函数写了一个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)
{
    int sfd,dfd;
    char buf[BUFSIZE];
    int len,ret,pos;

    if(argc < 3)
    {
        fprintf(stderr,"Usage...\n");
        exit(1);
    }
    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)
    {
        close(sfd);
        perror("open():");
        exit(1);
    }
    while (1)
    {
        len = read(sfd,buf,BUFSIZE);
        if(len < 0)
        {
            perror("read():");
            break;
        }
        if(len == 0)
            break;
        pos = 0; //记录要操作的位置
        //假如读到了10个,即len=10,而写的时候只写进去3个。为了保证下次都写进去
        while(len > 0)
        {
            ret = write(dfd,buf+pos,len);
            if(ret < 0)
            {
                perror("write():");
                exit(1);
            }
            pos += ret;
            len = len - ret;
        }
    }
    close(dfd);
    close(sfd);
    return 0;
}

3、文件IO与标准IO的区别

区别:文件IO即系统IO就是即命令即做,给命令就立马完成,而标准IO是有一个缓冲区的,等到缓冲区满的时候才去做。比如:送信的大爷跑邮局,来一封信就立马送去邮局,这就是系统IO。假设大爷一次能送50封信,(即缓冲区大小就是50),标准IO就是等到50封信再送去邮局。假设现在收到5封信,第六封信需要加急寄出,那么大爷就会带着这6封信去邮局。这个就叫作刷新缓冲区。

缓冲区刷新有:强制刷新,满了刷新,遇到换行符刷新等。

从这里可以看出文件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);
    return 0;
}

通过strace ./a.out命令可以追踪可执行程序的过程。在下图就可以看到标准IO是把它放到一个缓冲区里。而系统IO就是及时输出。

在这里插入图片描述

5、原子操作和程序中的重定向:dup、dup2

原子操作:不可分割的操作

原子:不可分割的最小单位

原子操作的作用:解决竞争和冲突

实例:dup.c

/*
 * 理解原子操作和dup,dup2方法是干嘛的
 * 实现在不改动****************下面内容的同时,把puts()里面的内容重定向到一个指定的文件中,而不是在终端输出
 */

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

#define FNAME "/home/zg/tmp/out"

int main()
{
    int fd;
    close(1);
    //fd文件描述符总是使用现在可用范围内最小的那个,关闭1(终端输出,stdout)下一个fd就用1
    fd = open(FNAME,O_WRONLY|O_CREAT|O_TRUNC,0700);
    if(fd < 0)
    {
        perror("open()");
        exit(1);
    }
//    close(1);
//   dup(fd);
		dup2(fd,1);
		if(fd != 1){
    close(fd);
	}
    //*******************************
    puts("hello!");
    exit(0);
}

上述代码有以下问题:

  • 如果fd本身就是1,即在没有打开终端stdout的情况,dup(fd)之前关闭了1,那么就会出错。
  • close(1)和dup(fd)是两步操作,不具有原子性。在Unix多进程多线程并发的情况下,可能在close(1)关闭之后,CPU调度到另一个线程执行,而另一个线程占用了这个文件描述符,等到CPU再次回到这个线程时,dup(fd)就会复制到另一个文件描述符,从而把hello输出到另一个文件中去了。

解决这种问题可以使用dup2方法,这个方法具有原子性。如果oldfd同newfd相同,则返回newfd而不关闭它。

在这里插入图片描述
在这里插入图片描述

6、同步问题:sync、fsync、fdatasync

当我们向文件写入数据时,内核通常先将数据复制到缓冲区,然后排入队列,晚些时候再写入磁盘。这种方式叫做**“延迟写”。**

当内核需要重用缓冲区来存放其他磁盘数据时,会把当前所有延迟写的数据写入到磁盘。而为了保证写入磁盘中的实际数据同缓冲区的内容一致,UNIX系统提供了sync、fsync和fdatasync三个函数。

在这里插入图片描述

fsync函数只对文件描述符指定的一个文件起作用,并且等待写入磁盘操作结束才返回。保证修改过的块能够立即写入磁盘。

8、管家级函数:fcntl()、ioctl()

在这里插入图片描述

fcntl()具体的使用参考书籍或者man手册。

ioctl()函数是针对设备的IO操作的管家函数。

9、/dev/fd/目录

这是一个虚目录,对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd。显示的是当前进程的文件描述符信息。

在这里插入图片描述

当前显示的就是ls命令实现所用到的文件描述符信息。谁看就是显示谁的。如果要在当前进程查看文件描述符信息,就在当前进程打开这个文件就可以看到。这些显示的都l开头的链接文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值