APUE-文件I/O

发布时间 2024-01-11 16:06:05作者: 不爱吃梨子

库函数和系统调用

库函数调用 系统调用
在所有的ANSI C编译器中,C库函数都是相同的 各个操作系统的系统调用是不同的,这导致程序不可移植
它调用库函数中的一段程序(或函数) 它调用系统内核的服务
与用户程序相联系 在内核地址空间执行
它的运行时间属于“用户时间” 运行时间属于“系统时间”
属于过程调用,调用开销较小 需要在用户控件和内核 上下文环境切换,开销较大
在C函数库libc中大约有300个函数 在UNIX中大约有90个系统调用
典型的C函数库:printf、fopen、fread、malloc 典型的系统调用:write、open、read、sbrk、fork

文件I/O系统调用

文件和文件描述符

文件扩展名

​ 在Linux中,扩展名对Linux内核没有实际意义,但是可以用来人为区分不同的文件,方便用户使用。

  • .tar,.tar.gz,.tgz,zip,.tar.bz表示压缩文件,创建命令为tar,gzip,unzip等
  • .sh文件表示shell脚本文件
  • .pl 表示perl语言文件
  • .py 表示python语言文件
  • .conf 表示系统服务的配置文件
  • .c表示C文件
  • .h头文件
  • .cpp表示C++源文件
  • .so 表示动态库文件
  • ·a 表示静态库文件

文件类型

​ Linux系统中把一切都看做文件,Linux有7中类型文件:普通文件-、目录(dierectory)文件、符号(link)链接、字符(character)设备文件、块(block)设备文件、管道(pipe)文件、套接字(socket)文件。其中文件、目录、符号链接会占用磁盘空间来存储,而块设备、字符设备、套接字、管道是伪文件,并不占用磁盘空间。

​ 通过ls -l 命令查看文件类型

文件类型标识 文件类型
- 普通文件
d 目录文件
l 符号链接
c 字符设备
b 块设备
p 管道
s 套接字socket

文件描述符

​ 文件描述符(file descriptor, fd) 是Linux内核为了高效管理已被打开的文件所创建的索引, 其是一个非负整数(通常是小整数) , 用于指代被打开的文件, 所有执行I/O操作的系统调用都通过文件描述符。程序在开始运行时,系统会自动打开三个文件描述符, 0是标准输入, 1是标准输出, 2是标准错误。POSIX标准要求每次打开文件时(含socket) 必须使用当前进程中最小可用的文件描述符号码, 因此第一次打开的文件描述符一定是3.

文件描述符 用途 POSIX文件描述符 标准I/O文件流
0 标准输入 STDIN_FILENO stdin
1 标准输出 STDOUT_FILENO stdout
2 标准出错 STDERR_FILENO stderr

文件I/O操作

程序编写

/*头文件包含,可以通过man手册查找*/
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/*MSG_STR是写到文件中的内容,是一个常量,会保存到程序的只读数据段中*/
#define BUFSIZE 1024
#define MSG_STR "Hello World\n"

int main(int argc,char *argv[])
{
        int fd = -1;	//文件描述符,正常应该为非负的整数
        int rv = -1;	//返回值return value
        char buf[BUFSIZE];	//存放从文件中读到的数据

        fd = open("test.txt",O_RDWR|O_CREAT|O_TRUNC,0666);	//调用open()系统调用并返回一个文件描述符保存在fd中
        if(fd < 0)
        {
            /*perror()函数可以打印出错的原因,但是不能处理要进行格式化控制的输出或写入日志文件 */      
                perror("Open/Creat file test.txt failure");      
                return 0;
        }
        printf("Open file returned file descriptor [%d]\n",fd);
        /* write(写到哪里,写什么,写的大小)
    	 *用strlen是因为MSG_STR是宏定义,宏定义是地址*/
        if((rv = write(fd,MSG_STR,strlen(MSG_STR))) < 0)
        {
            /*strerror()函数可以将整型类型的出错原因errno转换成相应的字符串形式*/
                printf("Write %d bytes into file failure: %s\n",rv,strerror(errno));
                goto cleanup;	//在做错误处理时一般会用goto,做集中的错误处理
        }
    
    /*test.txt里有成功写入,但总是Read 0 bytes data from file,这是因为在Hello World写入后,文件偏移量(类似光标)位于World的后方,读或写入时从光标所在位置开始读写,所以读不到值,通过lseek()系统调用将文件偏移量设置到第一个字节后,Read 12 bytes data from file: Hello World
*/
    //lseek(fd, 0, SEEK_SET);

    /*buf在栈中,栈中的数据是随机数,在使用buf时不对buf进行初始化,会导致后面打印出的是个随机数。因此在往buf中写内容前要先用memset将buf中的内容清掉*/
        memset(buf,0,sizeof(buf));	
        if( (rv=read(fd,buf,sizeof(buf))) < 0 )
        {
                printf("Read data from file failure: %s\n",strerror(errno));
                goto cleanup;
        }

        printf("Read %d bytes data from file: %s\n",rv,buf);

cleanup:
        close(fd);

        /*非main函数return只会导致函数退出
         *main函数的return会调用exit(),导致整个进程中止
         *同理,其他非main函数调用exit()一样会导致整个进程中止
         *程序结束后,可以通过"echo $?"命令查看返回值*/
        return 0;
}

strlen()和sizeof()的区别:

  • sizeof() 是一个运算符,而 strlen() 是一个函数。
  • sizeof() 计算的是变量或类型所占用的内存字节数,而 strlen() 计算的是字符串中字符的个数。
  • sizeof() 可以用于任何类型的数据,而 strlen() 只能用于以空字符 '\0' 结尾的字符串。
  • sizeof() 计算字符串的长度,包含末尾的 '\0',strlen() 计算字符串的长度,不包含字符串末尾的 '\0'。

程序编译运行

li@ubuntu22:~/apue$ gcc file_io.c -o file_io
li@ubuntu22:~/apue$ ./file_io
Open file returned file descriptor [3]
Read 0 bytes data from file:
li@ubuntu22:~/apue$ ./file_io
Open file returned file descriptor [3]	#再运行一次还是3,描述符是当前进程最小可用的,第二次运行是新的进程
Read 0 bytes data from file:
li@ubuntu22:~/apue$ cat test.txt #通过cat test. txt命令查看文件内容验证 "Hello World"字符串确实被wirte()系统调用写入到了文件中
Hello World

文件I/O操作函数

open()系统调用

int open(const char *path, int :flag, ... /*mode_t mode*/);

open()系统调用用来打开一个文件, 并返回一个文件描述符(file description), 并且该文件描述符是当前进程,最小、未使用的文件描述符数值。

参数: path: 要打开的文件、设备的路径
flag: 由多个选项进行 “或”运算构造flag参数。

​ 必选:O_RDONLY(只读)、 O_WRONLY(只写)、 O_RDWR(读写)

​ 可选:O_APPEND 每次写时都追加到文件的尾端。

​ O_CREAT 文件不存在则创建它, 使用该选项需要第三个参数mode

​ O_TRUNC 如果文件存在,而且为只写或读写成功打开,则将其长度截取为0

​ O_NONBLOCK 如果path是一个FIFO、块设备、字符特殊文件则此选项为文件的本次打开和后续的I/O操作设置非阻塞模式方式。

​ O_EXEC、O_SEARCH、O_CLOEXEC、O_NOCTTY....

mode: oflag带O_CREAT选项时可以用来创建文件, 这时必须带该参数用来指定创建文件的权限模式,如0666。否则不需要。使用示例代码:
int fd;
fd = open("text. txt", O_RDWR|O_CREAT|O_TRUNC,
0666);	//O_RDWR文件打开可以通过文件描述符可读可写,O_CREAT文件不存在则创建,O_TRUNC如果文件中有内容先把文件清掉
fd =open("text. txt", O_WRONLY|O_APPEND);	//O_WRONLY只写,O_APPEND追加到文件尾,如果文件不存在则返回-1

creat()系统调用

/*用来创建一个新文件并返回其fd,等价于open(path, O_WRONLY|O_CREAT|O_TRUNC,mode);*/
int creat(const char *path,mode_t mode);

close()系统调用

/*用来关闭一个打开的文件描述符*/
int close(int fd);

write()系统调用

/*用来往打开的文件描述符fd指向的文件中写入buf指向的数据,nbytes指定要写入的数据大小*/
size_t write(int fd,const void *buf,size_t nbytes);

read()系统调用

/*用来从打开的文件描述符对应的文件中读取数据放到buf指向的内存空间中去,最多不要超过nbytes个字节,nbytes一般是buf剩余的空间大小*/
size_t read(int fd,void *buf,size_t nbytes);

lseek()系统调用

off_t lseek(int fd, off_t offset, int whence);

我们在从文件里读出内容, 或往文件写如内容的时候都有一个起始地址, 这个起始地址就是当前文件偏移量,当我们对文件进行读写的时候都会使文件偏移量往后偏移。这点就类似于我们打开记事本开始编辑文本时的光标, 我们读或写入时从光标所在位置开始读写, 每读写一个字节都会使光标往后偏移。通过lseek()这个函数我们可以调整文件偏移量的地址。

其中 whence 可以是以下三个值

whence 位置
SEEK_SET 文件头
SEEK_CUR 当前位置
SEEK_END 文件尾

而offset就是相对于whence 的偏移量, 譬如:

lseek(fd, 0, SEEK_SET); 将文件偏移量设置到了文件开始的第一个字节上

lseek(fd, 0, SEEK_END); 将文件偏移量设置到文件最后一个字节上

lseek(fd, -1, SEEK_END); 将文件偏移量设置到文件最后的倒数第一个字节上

dup()和dup2()系统调用

int dup(int fd);
int dup2(int fd,int fd2);

这两个函数都可以用来复制一个新的文件描述符来指向fd对应的文件。这两个系统调用经常用在标准输入、标准输出、标准出错重定向。

dup0返回的新文件描述符一定是当前可用文件描述符中的最小数值

dup2可以用fd2参数来指定新的文件描述符。如果fd2已经打开,则先关闭。如fd等于fd2,则dup2返回fd2,而不关闭它。

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

int main(int argc,char *argv[])
{
        int fd = -1;

        fd = open("std.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
        if( fd < 0 )
        {
                printf("Open file failure: %s\n",strerror(errno));
                return 0;
        }

        close(0);	//用dup()先close()
        close(1);
        close(2);
		dup(fd);    //0 标准输入
		dup(fd);    //1 标准输出
		dup(fd);    //2 标准错误输出

//    dup2(fd, STDIN_FILENO);     // 标准输入重定向到fd
//    dup2(fd, STDOUT_FILENO);    // 标准输出重定向到fd
//    dup2(fd, STDERR_FILENO);    // 标准错误输出重定向到fd

        printf("fd=%d\n",fd);

        close(fd);
        return 0;
}
li@ubuntu22:~/apue$ vim redirect_stdio.c
li@ubuntu22:~/apue$ gcc redirect_stdio.c -o redirect_stdio
li@ubuntu22:~/apue$ ./redirect_stdio
li@ubuntu22:~/apue$ echo $?
0
li@ubuntu22:~/apue$ ls
file_io  file_io.c  redirect_stdio  redirect_stdio.c  std.txt  test.txt
li@ubuntu22:~/apue$ cat std.txt
fd=3

stat()和fstat()系统调用

int stat(const char *restrict path,struct stat *restrict buf);
int fstat(int fd,struct stat *buf);

用于返回文件或目录相关信息,stat()第一个参数是文件名,而fstat()第一个参数是文件打开的相应文件描述符。

struct stat结构体的定义:

struct stat {
    dev_t st_dev;         // 文件的设备 ID
    ino_t st_ino;         // 文件的 inode 号
    mode_t st_mode;       // 文件类型和权限
    nlink_t st_nlink;     // 文件的硬链接数目
    uid_t st_uid;         // 文件所有者的用户 ID
    gid_t st_gid;         // 文件所有者的组 ID
    dev_t st_rdev;        // 设备文件的设备 ID
    off_t st_size;        // 文件总大小(以字节为单位)
    blksize_t st_blksize; // 文件系统的 I/O 缓冲区大小
    blkcnt_t st_blocks;   // 分配给文件的磁盘块数
    time_t st_atime;      // 最后一次访问时间
    time_t st_mtime;      // 最后一次修改时间
    time_t st_ctime;      // 最后一次更改时间(修改文件的权限或者所有者)
};
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc,char **argv)
{
        struct stat stbuf;

        stat("stat.c",&stbuf);
    
        /*stbuf是一个指针,在栈区,它指向的地址是一个随机地址(野指针),使用指针一定要先初始化:
        1、 使用malloc
        	struct stat *stbuf;
        	stbuf = malloc(sizeof(struct stat))
        2、定义一个变量stbuf,传&stbuf
        	struct stat stbuf;
        	stat("stat.c",&stbuf);
        */
        //struct stat *stbuf;
        //stat("stat.c",stbuf);
    
    
    	/*fstat()要先用open()后使用*/
//    	int fd = -1;
//    	fd = open("stat.c",O_RSONLY);
//    	fstat(fd,stbuf);
    
    
        printf("File Mode: %o Real Size: %luB,Space Size: %luB\n",stbuf.st_mode,stbuf.st_size,stbuf.st_blksize);

        return 0;
}
li@ubuntu22:~/apue$ vim stat.c
li@ubuntu22:~/apue$ gcc stat.c -o stat
li@ubuntu22:~/apue$ ./stat
File Mode: 100664 Real Size: 283B,Space Size: 4096B
li@ubuntu22:~/apue$

access()系统调用

int access(const char *path,int mode)

用来测试文件是否存在或测试其权限位,其中第一个参数path是相应的文件路径名,第二个参数是要测试的模式。

mode说明:

模式 说明
R_OK 测试读许可权
W_OK 测试写许可权
X_OK 测试执行许可权
F_OK 测试文件是否存在
#include <stdio.h>
#include <unistd.h>

#define TEST_FILE "access.c"

int main(int argc,char *argv[])
{
    	//测试文件是否存在
        if(access(TEST_FILE,F_OK) != 0)
        {
                printf("File %s not exist!\n",TEST_FILE);
                return 0;
        }

        printf("File %s not exist!\n",TEST_FILE);

    	//测试读许可权
        if(access(TEST_FILE,R_OK)==0)
        {
                printf("READ OK\n");
        }
		//测试写许可权
        if(access(TEST_FILE,W_OK)==0)
        {
                printf("WRITE OK\n");
        }
		//测试执行许可权
        if(access(TEST_FILE,X_OK)==0)
        {
                printf("EXEC OK\n");
        }

        return 0;
}
li@ubuntu22:~/apue$ ./access
File access.c not exist!
READ OK
WRITE OK

unlink()系统调用

用来删除文件,其本质是让文件的链接记数自减,当链接记数减为0时,文件会被自动删除。(Linux下rm命令其实就是调用了unlink()系统调用)

rename()系统调用

将文件重命名

文件夹操作相关系统调用

函数原型 函数说明
int mkdir(const char *pathname,mode_t mode) 创建文件夹
int rmdir(const char *pathname) 删除文件夹(文件夹必须为空)
DIR *opendir(const char *pathname) 打开文件夹
struct dirent *readdir(DIR *dp) 读文件夹
int closedir(DIR *dp) 关闭文件夹
int chdir(const char *pathname) 改变工作目录

readdir()系统调用的struct dirent:

struct dirent
{
  long d_ino; /* inode number 索引节点号 */
    off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
    unsigned short d_reclen; /* length of this d_name 文件名长 */
    unsigned char d_type; /* the type of d_name 文件类型 */
    char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长255字符 */
}
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define TEST_DIR "dir"

int main(int argc,char *argv[])
{
        int rv = 0;
        int fd1 = -1;
        int fd2 = -1;
        DIR *dirp = NULL;
        struct dirent *direntp = NULL;

    	/* 创建文件夹,文件权限755 */
        if( mkdir(TEST_DIR,0755) < 0 )
        {
                printf("creat directory '%s' failure: %s\n",TEST_DIR,strerror(errno));
                return -1;
        }

    	/* 更改当前工作路径到文件夹dir下 */
        if( chdir(TEST_DIR) < 0 )
        {
                printf("change directory '%s' failure: %s\n",TEST_DIR,strerror(errno));
                rv = -2;
                goto cleanup;
        }
    
    	/* 在dir文件夹下 创建普通文本文件file1.txt,并设置其权限位为644 */
		if( (fd1 = creat("file1.txt",0644)) < 0 )
        {
                printf("creat file1.txt failure: %s\n",strerror(errno));
                rv = -3;
                goto cleanup;
        }

    	/* 在dir文件夹下 创建普通文本文件file2.txt,并设置其权限位为644 */
        if( (fd1 = creat("file2.txt",0644)) < 0 )
        {
                printf("creat file2.txt failure: %s\n",strerror(errno));
                rv = -4;
                goto cleanup;
        }

   		 /* 更改当前工作路径到父目录去 */
        if( chdir("../") < 0 )
        {
                printf("change directory to '%s' failure: %s\n",TEST_DIR,strerror(errno));
                rv = -5;
                goto cleanup;
        }

    	/* 打开dir文件夹 */
        if ((dirp = opendir(TEST_DIR)) == NULL)
        {
                printf("opendir %s error: %s\n", TEST_DIR, strerror(errno));
                rv = -6;
                goto cleanup;
        }
    
    	/* 列出dir里面的所有文件和文件夹,一个文件夹下至少要有.和..两个文件,从文件夹下读时,每个文件都是一个dirent*/
		while((direntp = readdir(dirp)) != NULL)
        {
                printf("find file: %s\n",direntp->d_name);
        }

    	/* 关闭所有打开的文件夹 */
        closedir(dirp);
        return rv;

cleanup:
        if(fd1 >= 0)
        {
                close(fd1);
        }

        if(fd2 >= 0)
        {
                close(fd2);
        }
}
li@ubuntu22:~/apue$ vim dir.c
li@ubuntu22:~/apue$ gcc dir.c -o dir
li@ubuntu22:~/apue$ ./dir
find file: file1.txt
find file: ..
find file: file2.txt
find file: .
li@ubuntu22:~/apue$ ls directory -la
total 8
drwxr-xr-x 2 li li 4096  9月 24 21:16 .
drwxrwxr-x 3 li li 4096  9月 24 21:16 ..
-rw-r--r-- 1 li li    0  9月 24 21:16 file1.txt
-rw-r--r-- 1 li li    0  9月 24 21:16 file2.txt