文章目录
一、标准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
参数解释:
若whence
是SEEK_SET
,则将文件的偏移量设置为距文件开始处offset
个字节。
若whence
是SEEK_CUR
,则将文件的偏移量设置为当前值加上offset,
其中offset
可为正或负。
若whence
是SEEK_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开头
的链接文件。