前面对UNIX的基础知识进行了简单介绍,接下来将介绍UNIX的细节部分。首先介绍文件I/O。
对于内核而言,所有打开的文件都通过文件描述符进行引用。文件描述符是一个非负整数,当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。
UNIX系统将文件描述符0于标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误相关联,但同时在符合POSIX.1
标准的应用程序中,0==STDIN_FILENO、1==STDOUT_FILENO、2==STDERR_FILEENO
.这些常量都在
中定义。
open
和openat
调用函数open
或openat
可以打开或创建一个文件。
#include
int open(const char* path,int oflag,...); //...表示余下的参数数量及类型时可变的,oflag参数用来说明函数的多个选项。
int openat(int fd,const char* path,int oflag,...);
若打开成功则返回文件描述符,不成功则返回-1.
oflag
常用参数:
O_WRONLY
:以只写模式打开文件。
O_RDWR
:以读写模式打开文件。
O_CREAT
:如果文件不存在,则创建文件。
O_EXCL
:与 O_CREAT
一起使用,如果文件已存在,则打开失败。
O_TRUNC
:如果文件已存在,将其截断为零长度。
O_APPEND
:在文件末尾追加内容,而不是覆盖原有内容。
O_SYNC
:每次写入操作都会等待物理 I/O 完成,确保数据被写入磁盘。
O_NONBLOCK
:以非阻塞方式打开文件。
O_RDONLY
:以只读方式打开文件
#include
#include"apue.h"
int main(int argc,char *argv[]) {
int fileDescriptor = open(argv[1], O_RDONLY);
if (fileDescriptor != -1) {
printf("打开成功\n");
close(fileDescriptor); // 关闭文件
} else {
printf( "无法打开文件!\n");
}
exit(0);
}
创建一个data文件进行验证,如图所示
另外oflag
参数还可以通过”或“运算符进行连接:
int fileDescriptor = open("example.txt", O_CREAT | O_WRONLY | O_TRUNC); //我们使用 O_CREAT 参数创建了一个新文件,并使用 O_WRONLY 表示以只写模式打开文件。另外,我们还使用 O_TRUNC 标志将文件截断为零长度(如果文件已存在)。
openat
函数相对于open
函数参数多了个fd
,具有额外的灵活性,可以基于给定的文件描述符或路径来打开文件。open
函数使用的是绝对路径,而 openat
函数可以使用相对路径,并以给定的文件描述符(通常是目录的文件描述符)作为参考点来解析路径。
openat
函数的使用有三种可能性:
path参数指定的是绝对路径名,这时fd
参数被忽略,相当于open函数
path参数指定的是相对路径名,fd
参数指出了相对路径名在文件系统中的开始地址。fd
参数是通过打开相对路径名所在的目录来获取。
path参数指定相对路径名,fd
参数具有特殊值AT_FDCWD
,openat
函数将使用当前工作目录作为参考点,解析相对路径名,并打开相应的文件。
#include
#include "apue.h"
int main(int argc,char* argv[]) {
// 示例 1:使用绝对路径名
int fileDescriptor1 = openat(AT_FDCWD,argv[1], O_RDONLY);
if (fileDescriptor1 != -1) {
printf("文件成功打开\n");
close(fileDescriptor1); // 关闭文件
} else {
printf("无法打开文件!\n");
}
exit(0);
}
将绝对路径作为参数传入:
#include
#include "apue.h"
// 示例 2:使用相对路径名和 fd 参数
int main(int argc,char* argv[]){
int dirfd = open(argv[1], O_RDONLY);
if (dirfd == -1) {
err_sys("无法打开目录 %s",argv[1]);
}
int fileDescriptor2 = openat(dirfd, argv[2], O_RDONLY);
if (fileDescriptor2 != -1) {
printf("文件成功打开\n");
close(fileDescriptor2); // 关闭文件
} else {
err_sys("无法打开文件%s",argv[2]);
}
close(dirfd); // 关闭目录
exit(0);
}
将相对路径作为参数传入:
#include
#include "apue.h"
int main(int argc,char* argv[]){
// 示例 3:使用相对路径名和 AT_FDCWD 参数
int fileDescriptor3 = openat(AT_FDCWD, argv[1], O_RDONLY);
if (fileDescriptor3 != -1) {
printf("文件成功打开\n");
close(fileDescriptor3); // 关闭文件
} else {
err_sys("无法打开文件%s",argv[1]);
}
exit(0);
}
将相对路径作为参数传入:
lseek
每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
#include
#include
off_t lseek(int fd, off_t offset, int whence);
fd
:文件描述符,表示要操作的文件。offset
:相对于 whence
的偏移量,可以是正数、负数或零。SEEK_SET
:从文件开头开始计算偏移量。SEEK_CUR
:从当前文件位置开始计算偏移量。SEEK_END
:从文件末尾开始计算偏移量。返回值:
errno
错误代码来指示错误类型。检测标准输入能否设置偏移量:
#include "apue.h"
int main(){
if (lseek(STDIN_FILENO,0,SEEK_CUR)==-1){
printf("cannot seek\n");
}else{
printf("seek OK\n");
}
exit(0);
}
./lseek1 < /etc/passwd //直接将 /etc/passwd 文件的内容作为 lseek1 可执行文件的标准输入
cat < /etc/passwd| ./lseek1 //首先使用 cat 命令读取 /etc/passwd 文件的内容,并将其输出到标准输出。然后,| 管道符将标准输出连接到 ./lseek1 可执行文件的标准输入。
lseek仅将当前文件的偏移量记录在内核中,并不引起任何I/O操作,该偏移量用于下一个读或写操作。
#include "apue.h"
#include
char buf1[]="abcdefghij";
char buf2[]="ABCDEFGHIJ";
int main(){
int fd;
if((fd=creat("file.hole",FILE_MODE))<0){ //调用 creat 函数创建了一个名为 "file.hole" 的文件,并将返回的文件描述符赋值给变量 fd。如果创建失败,则会调用 err_sys 函数打印错误信息并退出程序。
err_sys("creat error");
}
if(write(fd,buf1,10)!=10){ //调用 write 函数向文件写入 buf1 中的内容,写入长度为 10。如果写入失败,则会调用 err_sys 函数打印错误信息并退出程序。
err_sys("buf1 write error");
}
if(lseek(fd,16834,SEEK_SET)==-1){ //调用 lseek 函数将文件指针移动到偏移量 16834 处。SEEK_SET 表示从文件起始处计算偏移量。如果移动失败,则会调用 err_sys 函数打印错误信息并退出程序。
err_sys("lseek error");
}
if(write(fd,buf2,10)!=10){ //调用 write 函数向文件写入 buf2 中的内容,写入长度为 10。如果写入失败,则会调用 err_sys 函数打印错误信息并退出程序。
err_sys("buf2 write error");
}
exit(0);
}
vi lseek2.c
gcc lseek2.c -o lseek2 -l apue
./lseek2
ls -l file.hole
od -c file.hole
文件偏移量可以大于文件长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,位于文件中但没有写过的字节都被读为0.比如上述示例,首先往file.hole里面读取10个字节的数据,然后设置文件偏移量为16834,再接着往文件中读取10个字节数据。
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0040700 \0 \0 A B C D E F G H I J
40700对应位置就是16832,在过两个位置到A的位置也即16834,由此可见设置偏移量后文件中间位置都为空洞,写入从偏移位置开始写。
read
函数原型:
#include
ssize_t read(int fd, void *buf, size_t count);
参数说明:
fd
:要读取的文件描述符。buf
:用于存储读取数据的缓冲区。count
:要读取的最大字节数。read
函数会尝试读取最多count
个字节的数据,并将其存储到buf
指向的缓冲区中。它返回实际读取的字节数,若出错则返回-1。
在使用read
函数时,需要注意以下几点:
fd
参数必须是一个已经打开的文件描述符,例如通过open
函数或者其他文件操作函数获取的。buf
参数必须是一个有效的指针,指向足够容纳count
个字节的内存空间。read
函数是一个阻塞调用,当没有足够的数据可供读取时,它会一直等待,直到满足条件才返回。read
函数可能会读取少于请求的字节数,这是正常现象,因此在使用时需要根据返回值进行处理。write
write
函数是在 UNIX 操作系统中用于向文件描述符写入数据的函数。其函数原型如下:
#include
ssize_t write(int fd, const void* buffer, size_t count);
参数说明:
fd
:要写入的文件描述符。buffer
:要写入的数据的指针。count
:要写入的数据的字节数。write
函数会尝试将 buffer
指向的数据写入到文件描述符 fd
所描述的文件中。它返回实际写入的字节数,若出错则返回-1。
在使用 write
函数时,需要注意以下几点:
fd
参数必须是一个已经打开的文件描述符,例如通过 open
函数或其他文件操作函数获取的。buffer
参数必须是一个有效的指针,指向要写入的数据。count
参数表示要写入的字节数,如果写入成功,则返回写入的实际字节数,它可以少于请求的字节数,这是正常现象。write
函数通常是一个阻塞调用,它会一直等待直到写入的缓冲区内容被完全传输到内核缓冲区,然后才返回。buffer
指向的数据写入到文件描述符 fd
所描述的文件中。它返回实际写入的字节数,若出错则返回-1。在使用 write
函数时,需要注意以下几点:
fd
参数必须是一个已经打开的文件描述符,例如通过 open
函数或其他文件操作函数获取的。buffer
参数必须是一个有效的指针,指向要写入的数据。count
参数表示要写入的字节数,如果写入成功,则返回写入的实际字节数,它可以少于请求的字节数,这是正常现象。write
函数通常是一个阻塞调用,它会一直等待直到写入的缓冲区内容被完全传输到内核缓冲区,然后才返回。