《Linux – Linux高级编程 – 第一部分 标准IO及文件IO》第3章 文件I/O

3.1文件I/O概述

Linux操作系统是基于文件概念的。文件是以字符序列构成的信息载体。根据这一点,可以把I/O设备当做文件来处理,因此,在磁盘上的普通文件进行交互所用的统一系统调用可以直接用于I/O设备。这样大大简化了系统对于不同设备的处理,提高了效率。Linux中的文件主要分为6种:普通文件、目录文件、符号链接文件、管道文件、套接字文件和设备文件。

那么,内核如何区分和引用特定的文件呢?这里用到了一个重要的概念——文件描述符。对于Linux而言,所有的设备和文件的操作都是通过文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;读写文件时,需要把文件描述符作为参数传递给相应的函数。

在Linux中,所有打开的文件都对应一个文件描述符。文件描述符的本质就是一个非负数。当打开一个时,该整数由系统分配。文件描述符的范围是0-OPEN_MAX。早期的Unix版本的OPEN_MAX=19,即允许没个进程同时打开20个文件,现在很多系统则将其增加至1024个。

通常,一个进程启动时,都会打开3个流:标准输入、标准输出和标准错误。这3个流分别对应文件描述符0、1 和 2(对应的宏分别是STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO)。

基于文件描述符的I/O操作虽然不能直接移植到类Linux以外的系统上去(如Windows),但它往往是实现某些I/O操作的唯一途径,如Linux中底层文件操作函数、多路I/O、TCP/IP套接字编程接口等。同时,他们也很好地兼容POSIX标准,因此,可以很方便地移植到任何POSIX平台上。基于文件描述符的I/O操作是Linux中最常用的操作之一。

文件I/O相关函数:open() 、read() 、write() 、lseek() 和close() 。这些函数的特点是不带缓冲,直接对文件(包括设备)进行读写操作。这些函数不是ANSI C的组成部分,而是POSIX相关标准来定义。

3.2文件I/O操作

3.2.1文件打开与和关闭

open()函数用于创建或打开文件,在打开或创建文件时可以指定文件打开方式及文件的访问权限。

表1 open()函数
所需头文件 #include <sys/types.h>
#include<sys/stat.h>
#include
函数原型 int open(const char pathname, int flags);
int open(const char
pathname, int flags, mode_t mode);
函数参数 pathname: 被打开的文件名(可包括路径名)
flags(文件打开方式,这里介绍几个常用的)
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 可读可写
O_CREAT 如果文件不存在,就创建一个新文件,并用第三个参数为其设置权限;
O_TRUNC 若文件已经存在,那么会删除文件中的全部原有数据,并且设置文件大小为0;
O_APPEND 以添加方式打开文件,在写文件时,文件读写文职自动指向文件的末尾,即将写入的数据添加到文件的末尾;
mode :新建文件的存取权限
函数返回值 成功:返回文件描述符
失败:-1

close()函数用于关闭一个被打开的文件。当一个进程终止时,所有打开的文件都有内核自动关闭。很多程序都利用这一特性而不显示地关闭一个文件。

表2 close()函数
所需头文件 #include
函数原型 int close(int fd);
函数参数 fd:文件描述符
函数返回值 成功:0
失败:-1

3.2.2文件读写

read()函数从文件中读取数据存放到缓冲区中,并返回实际读取的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前读写位置开始读取数据,当前读写位置自动往后移动。

表3 read()函数
所需头文件 #include
函数原型 ssize_t read(int fd, void *buf, size_t count);
函数参数 fd :文件描述符;
buf :指定存储器读取数据的缓冲区;
count :指定读出的字节数;
函数返回值 成功:读到的字节数;
0:已到达文件尾;
1:出错;

在读到普通文件时,若读到要求的字节数之前已到达问价你的尾部,则返回的字节数会小于指定读出的字节数;

write()函数将数据写入文件中,并返回实际写入的字节数。写操作从文件的当前读写位置开始写入。对磁盘文件进行写操作时,若磁盘已满,write()函数返回失败;

表4 write()函数
所需头文件 #include
函数原型 ssize_t write(int fd, const void *buf, size_t count);
函数参数 fd :文件描述符;
buf :指定存储器写入数据的缓冲区;
count :指定读出的字节数;
函数返回值 成功:写入的字节数;
-1:出错;

下面写个简单小程序,实现copy程序,完成文件的复制,关于实例的流程如下所示。

6p8L8I.png

图1文件拷贝流程图

代码如下:

#include <stdio.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#define maxsize 256  
int main(int argc, char *argv[])  
{  
    int fd1,fd2;  
    int byte;  
    char buffer[maxsize];  
    if(argc != 3)  
    { 
        printf("command error!\n"); 
        return -1;  
    }    
    if((fd1 = open(argv[1],O_RDONLY)) == -1)  
    {  
            perror("open fails");  
            return -1;  
    }  
    //如果文件不存在,则创建,若存在,则覆盖;  
    if((fd2 = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC ,0664)) == -1)
    { 
        perror("open fails"); 
        return -1;  
    }  
    while(1)  
    {  
        if((byte = read(fd1,buffer,maxsize)) > 0)  
            write(fd2,buffer,byte);  
            if(byte == 0)  
                    break; //如果读不到数据,则返回  
    }  
    close(fd1);  
    close(fd2);  
    return 0;  
}  

执行结果如下:

6p8xr8.png

我们可以看到,原来file2.c并不存在,执行完程序后,file2.c存在,且大小和file1.c相同。

3.2.3文件定位

lseek()函数对文件当前读写位置进行定位。它只能对可定位(可随机访问)文件操作。管道、套接字和大部分字符设备文件不支持此类操作;

表5 lseek()函数
所需头文件 #include <sys/types.h>
#include
函数原型 off_t lseek(int fd, off_t offset, int whence);
函数参数 fd :文件描述符
offset :相对于基准点whence 的偏移量。以字节为单位,正数表示向前移动,负数表示向后移动
whence :当前位置的基点
SEEK_SET:文件的起始位置
SEEK_CUR:文件当前读写位置
SEEK_END:文件的结束位置
函数返回值 成功:文件当前读写位置
-1:出错

我们可以通过lseek函数实现一个小功能:查看文件的大小,代码如下:

#include <stdio.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <sys/types.h>  
int main(int argc, const char *argv[])  
{  
    int fd;  
    int length;  
    if(argc != 2)  
    {  
        printf("command error!\n");  
        return -1;  
    }  
    if((fd = open(argv[1],O_RDONLY)) == -1)  
    {  
        perror("open fails");  
        return -1;  
    }  
    length = lseek(fd,0,SEEK_END);  
    printf("The length of %s is %d bytes!\n",argv[1],length); 
    return 0;  
}  

执行结果如下:

6pGKIJ.png

我们可以看到,得到了lseek.c正确大小!

3.3文件与目录

3.3.1文件信息

前前在讲解文件基础的时候对文件的属性做了详细的安介绍,我们通过ls命令查看到的文件信息,也可以使用stat函数组提取出来。

我们首先使用命令来查看吧,通过前面的内容我们知道,一个文件对应一个索引号inode,inode是文件系统提供的唯一数值编址,这个数值叫inode编号(索引号),我们通过使用命令"ls -i""ls -al""ls -ail"可以查看索引号等元数据。

6pGlGR.png

【注1】发现sys和proc的inode(1)是相同的,也就说明了,只在磁盘上存了一份。
【注2】-i表示查看索引信息,-a表示查看所有,-l表示显示每个文件的详细信息。

接下来我们通过stat函数组来取得指定文件的文件属性,文件属性存储在结构体stat里。首先看看stat 结构体:

struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* inode number */
    mode_t    st_mode;        /* protection */
    nlink_t   st_nlink;       /* number of hard links */
    uid_t     st_uid;         /* user ID of owner */
    gid_t     st_gid;         /* group ID of owner */
    dev_t     st_rdev;        /* device ID (if special file) */
    off_t     st_size;        /* total size, in bytes */
    blksize_t st_blksize;     /* blocksize for filesystem I/O */
    blkcnt_t  st_blocks;      /* number of 512B blocks allocated */

    /* Since Linux 2.6, the kernel supports nanosecond
    precision for the following timestamp fields.
    For the details before Linux 2.6, see NOTES. */

    struct timespec st_atim;  /* time of last access */
    struct timespec st_mtim;  /* time of last modification */
    struct timespec st_ctim;  /* time of last status change */

    #define st_atime st_atim.tv_sec      /* Backward compatibility */
    #define st_mtime st_mtim.tv_sec
    #define st_ctime st_ctim.tv_sec
};

stat函数组有如下函数:

int stat(const char *path, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *path, struct stat *statbuf);

表6 stat()函数
所需头文件 #include <sys/types.h>
#include <sys/stat.h>
#include
函数原型 int stat(const char path, struct stat statbuf);
函数参数 pathname:文件路径
buf:文件信息
函数返回值 0:成功
-1:出错

表7 fsstat()函数
所需头文件 #include <sys/types.h>
#include <sys/stat.h>
#include
函数原型 int fstat(int fd, struct stat *statbuf);
函数参数 fd :文件描述符
*buf:文件信息
函数返回值 0:成功
-1:出错

表8 lstat()函数
所需头文件 #include <sys/types.h>
#include <sys/stat.h>
#include
函数原型 int lstat(const char path, struct stat statbuf);
函数参数 path:文件路径
buf:返回文件的信息,针对符号链接,lstat 返回链接本身,而不是而非目标文件
函数返回值 0:成功
-1:出错

看个例子吧。

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

int main(int argc,char *argv[])
{
    struct stat groupstat;
    int fd,ret;

    if(argc <2)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //stat函数测试  
    ret = stat(argv[1],&groupstat);
    if(ret)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    printf("stat function test , %s of st_ino inode is %ld\n",argv[1],groupstat.st_ino);

    //fstat函数测试
    fd = open(argv[1],O_RDWR|O_NOCTTY|O_NDELAY);
    if(fd<0)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    ret = fstat(fd,&groupstat);
    if(ret){
        printf("Please make sure file path\n");
        return 1;
    }
    printf("fstat function test , %s of st_ino inode is %ld\n",argv[1],groupstat.st_ino);

    //lstat函数测试 
    ret = lstat(argv[1],&groupstat);
    if(ret)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    printf("lstat function test , %s of st_ino inode is %ld\n",argv[1],groupstat.st_ino);
    return 0;
}

结果如下所示。

6pGNZD.png

我们通过命令查看是否一致呢?

6pGaIH.png

不管用那种方式,都能提取文件的索引号。

3.3.2文件权限

文件的权限我们通过ls查看,使用chmod可以修改文件的权限。关于chmod我们再Linux基础我们已经详细说明了,接下来我们要是用的是chmod函数组。

表9 chmod函数
所需头文件 #include <sys/stat.h>
函数原型 int chmod(const char *path, mode_t mode);
函数参数 *path:文件路径。
mode:直接使用数字即可。和前面命令中chmod 777 xxx 中的777 这个参数含义类似,也可用组合值。
函数返回值 0:成功
-1:出错

表10 fchmod函数
所需头文件 #include <sys/stat.h>
函数原型 int fchmod(int fd, mode_t mode)
函数参数 fd:文件描述符。
mode:直接使用数字即可。和前面命令中chmod 777 xxx 中的777 这个参数含义类似,也可以用组合值
函数返回值 0:成功
-1:出错
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char *argv[])
{
    int fd,ret;

    if(argc <3){
        printf("\nPlease input file path\n");
        return 1;
    }

    //chmod函数测试 
    ret = chmod(argv[1],0777);
    if(ret<0){
        printf("Please make sure file path\n");
        return 1;
    }
    printf("chmod %s 0777 is success!\n",argv[1]);

    //fchmod函数测试        
    fd = open(argv[2],O_RDWR|O_NOCTTY|O_NDELAY);
    if(fd<0)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    ret = fchmod(fd,0555);
    if(ret<0)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    printf("fchmod %s 0555 is success!\n",argv[1]);

    return 0;
}

结果如下所示:

6pGsQP.png

我们可以发现文件的权限被修改了。

3.3.3创建与删除目录

 创建目录
Linux命令中可以使用mkdir创建目录,系统编程中可以通过mkdir创建目录,这里我们使用mkdir函数创建目录。

表11 mkdir函数
所需头文件 #include <sys/stat.h>
#include <sys/types.h>
函数原型 int mkdir(const char *pathname, mode_t mode);
函数参数 pathname:文件路径
mode:直接使用数字即可。和前面命令中chmod 777 xxx 中的777 这个参数含义类似,也可以使用组合值。
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(int argc,char *argv[])
{
    int ret;

    //检测参数  
    if(argc <2){
        printf("\nPlease input file path\n");
        return 1;
    }

    //使用mkdir函数新建目录
    ret = mkdir(argv[1],0777);
    if(ret<0)
    {
        printf("mkdir %s failed!\n",argv[1]);
        return 1;
    }
    printf("mkdir %s suceces!\n",argv[1]);

    return 0;
}

结果如下所示:

6pGhJs.png

 删除目录
Linux命令中可以使用rmdir删除目录,系统编程中可以通过rmdir删除目录。

表12 rmdir函数
所需头文件 #include
函数原型 int rmdir(const char *pathname);
函数参数 pathname:文件和目录的路径
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    int ret;

    //检测参数  
    if(argc <2)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //使用rmdir函数删除目录
    ret = rmdir(argv[1]);
    if(ret<0)
    {
        printf("rmdir %s failed!\n",argv[1]);
        return 1;
    }
    printf("rmdir %s suceces!\n",argv[1]);

    return 0;
}

结果如下所示:

6pGTyV.png

3.3.4获取当前目录及目录切换

 获取当前路径
Linux命令中可以使用pwd查看当前目录,系统编程中可以通过getcwd获取当前目录。

表13 getcwd函数
所需头文件 #include
函数原型 char getcwd(char buf, size_t size);
函数参数 *buf:保存当前目录的缓冲区
size:在现代linux 中,buf 的长度至少可以为255 字节
函数返回值 成功返回指向当前目录的指针,和buf 的值一样
错误返回NULL

表14 get_current_dir_name函数
所需头文件 #include
函数原型 char *get_current_dir_name(void);
函数参数
函数返回值 成功返回指向当前目录的指针
错误返回NULL
#include <stdio.h>
/*getcwd、getwd和get_current_dir_name函数的头文件*/
#define __USE_GNU
#include <unistd.h>

#define LENTH 255

int main()
{
    char pwd[LENTH];
    char *wd;
    //getcwd函数测试        
    if(!getcwd(pwd,LENTH)){
        perror("getcwd");
        return 1;
    }
    printf("\ngetcwd pwd is %s\n",pwd);

    //get_current_dir_name函数测试  
    wd = get_current_dir_name();
    if(!wd)
    {
        perror("getcwd");
        return 1;
    }
    printf("\nget_current pwd is %s\n",wd);

    return 0;
}

结果如下所示:

6pGbeU.png

我们通过pwd查看当前路径。

6pGXFJ.png

值得注意的是如果在链接文件中getpwd能获得被链接文件路径,而get_current_pwd与pwd命令都是获得当前的路径。

6pGvWR.png

 目录切换

我们可以使用chdir函数组改变路径。

表15 chdir函数
所需头文件 #include
函数原型 int chdir(const char *path);
函数参数 path:文件当前路径
函数返回值 成功返回0
错误返回-1

表16 fchdir函数
所需头文件 #include
函数原型 int fchdir(int fd);
函数参数 fd:文件描述符
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define LENTH 255

int main(int argc,char *argv[])
{
    int ret;
    char pwd[LENTH];

    //检测参数  
    if(argc <3)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //getcwd函数获取当前目录        
    if(!getcwd(pwd,LENTH))
    {
        perror("getcwd");
        return 1;
    }
    printf("\ngetcwd pwd is %s\n",pwd);

    //使用chdir函数转入其他目录
    ret = chdir(argv[1]);
    if(ret)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    printf("chdir %s is success!\n",argv[1]);

    //转入其他目录,完成操作
    //使用rmdir函数删除目录
    ret = rmdir(argv[2]);
    if(ret<0)
    {
        printf("rmdir %s failed!\n",argv[2]);
        return 1;
    }
    printf("rmdir %s is success!\n",argv[2]);

    //再次使用chdir回到pwd保存的目录
    ret = chdir(pwd);
    if(ret)
    {
        printf("Please make sure file path\n");
        return 1;
    }
    printf("chdir %s is success!\n",pwd);

    return 0;
}

结果如下所示:

6pJPeO.png

3.3.5 opendir和closedir目录

前面介绍open和close函数用于打开关闭文件,这里介绍的opendir和closedir用于打开目录,相当于ls命令。

表17 opendir函数
所需头文件 #include <sys/types.h>
#include
函数原型 DIR opendir(const char name);
函数参数 name:目录的路径
函数返回值 成功返回指向目录流的指针
错误返回NULL

表18 closedir函数
所需头文件 #include <sys/types.h>
#include
函数原型 int closedir(DIR *dirp);
函数参数 dirp:opendir 返回的dir 指针
函数返回值 成功返回0
失败返回-1
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>

int main(int argc,char *argv[])
{
    int ret;
    DIR *dir;

    //检测参数  
    if(argc <2)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //使用opendir函数打开目录
    dir = opendir(argv[1]);
    if(dir==NULL)
    {
        printf("opendir %s failed!\n",argv[1]);
        return 1;
    }
    printf("opendir %s suceces!\n",argv[1]);

    //使用closedir函数关闭目录
    closedir(dir);

    return 0;
}

结果如下所示:

6pJiwD.png

3.3.6读取目录

在前面使用opendir打开目录的基础上,可以使用readdir读取目录信息。

表19 readdir函数
所需头文件 #include
函数原型 struct dirent readdir(DIR dirp);
函数参数 dirp:opendir 函数打开目录后返回的文件指针
函数返回值 成功返回指向dirp 的指针dirent
错误返回NULL
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>

int main(int argc,char *argv[])
{
    int ret;
    DIR *dir;
    struct dirent *catlog;
    //检测参数  
    if(argc <2)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //使用opendir函数打开目录
    dir = opendir(argv[1]);
    if(dir==NULL)
    {
        printf("opendir %s failed!\n",argv[1]);
        return 1;
    }
    printf("opendir %s suceces!\n",argv[1]);

    //使用readdir读取目录argv[1]
    catlog = readdir(dir);
    if(catlog == NULL)
    {
        printf("readdir %s failed!\n",argv[1]);
        return 1;
    }
    printf("%s d_ino is %ld\n ",argv[1],catlog->d_ino);

    //使用closedir函数关闭目录
    closedir(dir);

    return 0;
}

结果如下所示:

6pJEYd.png

3.3.7目录链接

 硬链接
我们可以使用ln进行硬链接,下面我们要讲解的是link函数。

表20 link函数
所需头文件 #include
函数原型 int link(const char oldpath, const char newpath);
函数参数 oldpath:已有的文件路径。
newpath:新建的硬链接文件路径。
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    int ret;

    if(argc <3)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //测试link函数
    ret = link(argv[1],argv[2]);
    if(ret)
    {
        printf("link failed");
        return 1;
    }
    printf("link %s to %s success!\n",argv[1],argv[2]);

    return 0;
}

结果如下所示:

6pJtlq.png

linktest.txt是新建的文件,linktest是连接后的文件。打开linktest文件可以发现和linktest.txt内容一样。linktest和linktest.txt索引信息也是一样的。

 符号链接

符号链接也叫软链接,可以使用symlink函数进行软链接。

表21 symlink函数
所需头文件 #include
函数原型 int symlink(const char oldpath, const char newpath);
函数参数 oldpath:已有的文件路径。
newpath:新建的硬链接文件路径。
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    int ret;

    if(argc <3)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //测试symlink函数
    ret = symlink(argv[1],argv[2]);
    if(ret)
    {
        printf("symlink failed");
        return 1;
    }
    printf("symlink %s to %s success!\n",argv[1],argv[2]);

    return 0;
}

结果如下所示:

6pJw0U.png

我们可以发现symlinktest.txt和symlinktest的索引信息不一样。那么硬链接和软连接有啥其他区别呢?其实硬链接就是给文件重名了,而软连接就是是一个快捷方式。

 解除链接
前面使用函数进行了硬链接和软链接,接下我们使用unlink函数来解除链接。

表22 unlink函数
所需头文件 #include
函数原型 int unlink(const char *pathname);
函数参数 pathname:链接文件的路径
函数返回值 成功返回0
错误返回-1

【注】unlink指向软链接,删除软链接;指向最后一个硬链接,相当于删除文件。

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

int main(int argc,char *argv[])
{
    int ret;

    if(argc <2)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    //测试unlink函数
    ret = unlink(argv[1]);
    if(ret)
    {
        printf("unlink failed");
        return 1;
    }
    printf("unlink %s is success!\n",argv[1]);

    return 0;
}

3.3.8移动或重命名文件及目录

移动文件或重命名文件命令为mv,函数为rename。

表23 rename函数
所需头文件 #include
函数原型 int rename(const char oldpath, const char newpath)
函数参数 oldpath:旧的文件路径
newpath:新的文件路径
函数返回值 成功返回0
错误返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

//argv[1] is oldpath ; argv[2] is newpath
int main(int argc,char *argv[])
{
    int ret;
    if(argc < 3)
    {
        printf("\nPlease input file path\n");
        return 1;
    }

    if(ret = rename(argv[1],argv[2]))
    {
        printf("\nerr\n");
    }

    printf("rename %s to %s success!\n",argv[1],argv[2]);
    return 0;
}

结果如下所示:

6pJDk4.png

Related posts

Leave a Comment