第五章
5-1 请使用标准I/O系统调用(open和lseek)和off_t数据类型修改程序清单5-3中的程序。将宏_FILE_OFFSET_BITS的值设置为64进行编译,并测试该程序是否能够成功创建一个大文件。
很遗憾的是,如今大部分系统为64位。在这些系统上安装较新版本的Linux系统,即使不做处理,程序也能创建超大文件。
将5-3程序进行修改之后,如下:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
// #define _FILE_OFFSET_BITS 64
void printfAndExit(const char *s, ...){
va_list list;
va_start(list, s);
vprintf(s, list);
va_end(list);
_exit(0);
}
int main(int argc, const char * argv[]){
int fd;
off_t off;
if(argc !=3 || strcmp(argv[1], "--help") == 0){
printfAndExit("%s pathname offset\n", argv[0]);
}
fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if(fd == -1)
printfAndExit("打开失败\n");
off = atoll(argv[2]);
if(lseek(fd, off, SEEK_SET) == -1)
printfAndExit("lseek失败\n");
if(write(fd, "test", 4) == -1)
printfAndExit("无法写入\n");
close(fd);
return 0;
}
编译并运行该程序,运行命令$ ./a.out x 10000000000000
,在一台安装了deepin 15版本的64位系统上可以直接创建一个大小接近10TB的文件。然而,当参数继续增大,尝试创建100TB的的文件时,无法创建成功。需要注意的是,虽然文件是10TB,但是文件具有空洞,所以并不会占用10TB的存储空间。
按照题目要求,我们可以将第8行的注释解除,或者在编译的时候使用$ gcc -D_FILE_OFFSET_BITS=64 5-1.c
进行编译,此时程序使用了64长度的文件尺寸。由于现在的操作系统大部分是64位的,因此我们看不出效果。如果在32位上进行同样的操作,我们可以发现程序能够生成的文件尺寸超过了2GB这个尺寸。
5-2 编写一个程序,使用O_APPEND标志并以写方式打开一个已存在的文件,且将文件偏移量置于文件起始处,在写入数据。数据会显示在文件中的哪个位置?为什么?
在设置了O_APPEND标志后,对打开文件的写入操作write会变成一个包含有文件偏移和写入的一个原子操作,强制在文件的结尾写入,所以在写入之前使用lseek也没有办法改变写入的位置。但是可以通过lseek在文件的任意位置读取。
5-3 编写程序,接受最多3个命令参数
$ atomic_append filename num-bytes [x]
该程序打开或创建指定文件,然后每次调用write()写入一个字节的方式,向文件结尾添加num-bytes个字节。若没有参数x,使用O_APPEND标志打开文件,否则不使用该标志打开文件。而是使用lseek(fd,0,SEEK_END)并write来追加内容。运行如下命令,解释ls -l两个文件大小的为什么不同。
$ atomic\_append f1 1000000 & atomic\_append f1 1000000
$ atomic\_append f2 1000000 x & atomic\_append f2 1000000 x
接下来我们开始编写程序。程序非常容易实现:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
struct atomic_append_struct
{
int fd;
int x;
const char *path;
int num_bytes;
} atomic_append_info;
int main(int argc, const char *argv[])
{
if (argc < 3)
{
printf("参数太少!\n");
_exit(0);
}
atomic_append_info.path = argv[1];
atomic_append_info.num_bytes = atoi(argv[2]);
if (argc == 4 && strcmp(argv[3], "x") == 0)
{
atomic_append_info.x = 1;
}
else
{
atomic_append_info.x = 0;
}
printf("path %s, num-bytes %d, x %d\n", atomic_append_info.path,
atomic_append_info.num_bytes, atomic_append_info.x);
int openFlag;
if (atomic_append_info.x)
{
openFlag = O_RDWR | O_CREAT;
}
else
{
openFlag = O_RDWR | O_CREAT | O_APPEND;
}
atomic_append_info.fd = open(atomic_append_info.path, openFlag, 0777);
if(atomic_append_info.fd == -1){
printf("打开文件失败\n");
_exit(0);
}
char buff = 'a';
for (int i = 0; i < atomic_append_info.num_bytes; i++)
{
if (atomic_append_info.x)
{
if(lseek(atomic_append_info.fd, 0, SEEK_END)==-1){
printf("无法lseek\n");
_exit(0);
}
}
if(write(atomic_append_info.fd, &buff, 1) == -1){
printf("无法写入\n");
_exit(0);
}
}
close(atomic_append_info.fd);
return 0;
}
编译程序:
$ gcc -o atomic_append atomic_append.c
运行程序
$ ./atomic_append f1 1000000 & ./atomic_append f1 1000000
$ ./atomic_append f2 1000000 x & ./atomic_append f2 1000000 x
查看文件大小
$ ls -l
可以看到文件大小有一些差异。这是因为使用&命令同时运行两个命令,如果第一个进程执行到lseek和write之间,被执行相同代码的第二个进程中断,那么第二个进程会在写入数据前将lseek放置在同样的位置写入。当第一个进程恢复后,重新覆盖了第二个程序写入的数据。这样就导致了数据写入的丢失。在我这里测试的结果表明,大约有3000次的数据丢失了,发生的概率是%0.15。如果操作系统每次都总能保证lseek和write同时进行,那么就不会出现这种情况,但是由于系统的执行顺序是未知的,才会导致了这种情况的发生。在使用了O_APPEND之后,就可以避免这样的事情发生,因为write原子操作已经包含了偏移操作。
5-4 使用fcntl()和close()来实现dup()和dup2()。dup2()在处理oldfd和newfd相等时,应检查oldfd是否有效,测试fcntl(oldfd, F_GETFL)是否成功就可以达到这一点。若oldfd无效,则dup2返回-1,并将errno置为EBADF。
dup命令传入oldfd,返回一个新的文件描述符,指向原来的文件描述符指向的句柄。
dup2命令传入oldfd,也是要返回一个复制过的oldfd,但是指定返回必须是newfd,如果newfd已经被打开了,那么会自动close这个fd。
程序也是比较简单的,如下:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int dup(int old){
int new;
new = fcntl(old, F_DUPFD, 0);
return new;
}
int dup2(int old, int new){
if(old == new){
if(fcntl(old, F_GETFL, 0) == -1){
errno = EBADF;
printf("error F_GETFL\n");
return -1;
}
return new;
}
close(new);
new = fcntl(old, F_DUPFD, new);
if(new == -1){
printf("error F_DUPFD\n");
_exit(0);
}
return new;
}
int main(void){
int fd = dup2(0,0);
printf("fd = %d\n",fd);
write(fd, "hello world!\n", 13);
fd = dup(0);
printf("fd = %d\n",fd);
write(fd, "hello world!\n", 13);
close(fd);
return 0;
}
在上面这个程序中,复制的文件描述符是0,也就是标准输入。
5-5 编写一程序,验证文件描述符及其副本是否共享了文件偏移量和打开文件的状态标识。
使用lseek偏移0个位置就可以获取当前文件偏移量了。打开文件状态可以由fcntl获取。
程序如下,该程序打开程序源代码,然后每隔1s钟偏移一次,并输出文件的打开标志与偏移量。
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(void){
int fd1 = open("5-5.c", O_RDONLY);
int fd2 = dup(fd1);
while(1){
lseek(fd1, 1, SEEK_CUR);
lseek(fd2, 1, SEEK_CUR);
int flag1 = fcntl(fd1, F_GETFL);
int flag2 = fcntl(fd2, F_GETFL);
int lseek1 = lseek(fd1,0,SEEK_CUR);
int lseek2 = lseek(fd2,0, SEEK_CUR);
printf("%d %d %d %d\n", flag1, flag2, lseek1, lseek2);
sleep(1);
}
return 0;
}
5-6 说明下列代码中每次执行write后,输出文件的内容是什么,为什么。
fd1 = open(file, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
fd2 = dup(fd1);
fd3 = open(file, O_RDWR);
write(fd1, "Hello,", 6);
write(fd2, " world", 6);
//这里按照答案的解释,应该是有一个空格在world前面的,书上可能是印错了
lseek(fd2, 0, SEEK_SET);
write(fd1, "HELLO,", 6);
write(fd3, "Gidday", 6);
fd1文件是以可读可写打开的,打开时清空文件内容(O_TRUNC),fd2是fd1的复制,fd3再次将该文件打开。fd1和fd2拥有同一份文件句柄,fd3拥有自己的文件句柄。对fd1和fd2操作是在同一个偏移量和打开标志算的,对fd3操作是自己的偏移量和自己的标志。第一次write,文件变成了Hello,
,第二次写入,继续在后面加上world
,注意这里应该是有一个空格的,否则最后和答案对不上。第三次write,由于偏移至头,所以第三次覆盖了第一次的6个字符,变成了HELLO, world
,第四次fd3的偏移量是0,所以也是从头开始写,覆盖了第三次的HELLO变成了Gidday world
。
5-7 使用read()、write()、malloc()实现readv,writev函数功能。
程序比较简单,readv就是从其中读入结构体,writev就是先写入缓冲区然后一次写入。
#include <stdio.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
int myreadv(int fd, struct iovec *iov, int len)
{
int totalLen = 0;
for (int i = 0; i < len; i++)
{
totalLen += iov[i].iov_len;
}
for (int i = 0; i < len; i++)
{
for (int j = 0; j < iov[i].iov_len; j++)
{
if (read(fd, iov[i].iov_base + j, 1) == -1)
{
printf("读取失败\n");
_exit(0);
}
}
}
return totalLen;
}
int mywritev(int fd, struct iovec *iov, int len)
{
int totalLen = 0;
for (int i = 0; i < len; i++)
{
totalLen += iov[i].iov_len;
}
void *buff = malloc(totalLen);
int start = 0;
for (int i = 0; i < len; i++)
{
memcpy(buff + start, iov[i].iov_base, iov[i].iov_len);
start += iov[i].iov_len;
}
if (write(fd, buff, totalLen) == -1)
{
printf("写入失败\n");
_exit(0);
}
free(buff);
return totalLen;
}
int main(void)
{
char buf1[5], buf2[10];
struct iovec iov[2];
iov[0].iov_base = buf1;
iov[0].iov_len = 5;
iov[1].iov_base = buf2;
iov[1].iov_len = 10;
int fd = open("a.txt", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
int rsize = myreadv(fd, iov, 2);
printf("rsize = %d\n", rsize);
close(fd);
fd = open("b.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd < 0)
{
perror("open");
return -1;
}
int wsize = mywritev(fd, iov, 2);
printf("wsize = %d\n", wsize);
close(fd);
return 0;
}
注意运行时需要在同目录放一个a.txt
文件,并在内容中填充任意大于15个字节的东西即可。程序首先将读入数据,然后写入到b.txt
文件。