linux 基础编程:文件操作总结

时间:2022-10-06 10:03:42

最近在看底层系统相关开源代码的时候,发现自己linux下面的基础编程真的没有过关,重新捧起《linux程序设计,第三版》好好学习一下基础编程,文件编程当然是第一站咯,今天看完,好好总结一下吧。

小条例

一切皆文件:在linux操作系统中,文件为操作系统服务和设备提供了一个简单而统一的接口,即一切皆文件。应用程序可以像使用文件一样使用磁盘文件,串行口,打印机和其他设备。目录也是一种特殊文件。

系统调用和标准库:在一个进程中,对文件进行访问可以包括两部分。第一部分通过调用“系统调用”或者设备驱动程序接口等相关函数,切换到内核空间,进行访问文件,该方法相对于函数调用来说,需要在用户代码和内核代码切换,效率底下(减小开销的方法:让每次系统调用完成尽可能多的功能,比如每次读取大量数据而不是一个字符)。第二部分是在用户空间里,调用标准库函数里面的函数来访问文件,标准库函数是在系统调用的基础上,提供了缓存功能,减少系统调用的次数,经而提供程序的效率。

文件描述符:在进程中,通过文件描述符来关联已经打开的文件,文件描述符是一个小值整数,通过文件描述符,可以访问已经打开的文件或者设备。在程序启动时候,默认打开3个描述符:0:标准输入,1:标准输出,2:标准错误。在标准库中通过FILE *结构体来封装文件描述,对应默认的三个描述符:stdin,stdout,stderr。

文件操作头文件

#include<stdio.h>//标准库头文件
#include<dirent.h>//目录相关操作函数
#include<unistd.h>//文件系统调用相关函数
#include<sys/stat.h>//文件属性相关标志位的宏定义
#include<sys/types.h>//linux标准类型定义和宏定义
#include<errno.h>//错误代码

文件stat属性结构

文件stat结构:该结构中包含了文件属性信息,可以通过下面函数进行得到

#include <unistd.h>
#inlcude <sys/stat.h>
#include <sys/types.h>

int fstat(int fildes,struct stat *buff);
int stat(const char *path,struct stat *buff);
int lstat(const char *path,struct stat *buff);
stat和lstat以文件目录为参数,如果文件是符号链接时,lstat返回的是符号链接信息,而stat返回原始文件的信息。其他情况下面,两个函数返回结果一样,下面主要分析一下stat结构,首先看stat结构体:

struct stat
{
dev_t st_dev; /* 文件所在设备的ID*/
ino_t st_ino; /* iinode节点信息*/
mode_t st_mode; /* 文件权限和文件类型信息*/
nlink_t st_nlink; /* 链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user id*/
gid_t st_gid; /* group id*/
dev_t st_rdev; /* 设备号,针对设备文件*/
off_t st_size; /* 文件大小,字节为单位*/
blksize_t st_blksize; /* 系统块的大小*/
blkcnt_t st_blocks; /* 文件所占块数*/
time_t st_atime; /* 最后一次访问时间*/
time_t st_mtime; /* 内容最后一次修改时间*/
time_t st_ctime; /* 文件的权限,属组或内容最后一次修改的时间- */
};
其中mode_t是一个比较重要的内容,它存储了文件类型和文件权限相关信息。下面对其进行一个简单分析

mode_t是32位的整形变量类型,各位代表的信息如下。

           S_IFMT     0170000   利用该掩码来检查文件类型
S_IFSOCK 0140000 socket文件
S_IFLNK 0120000 符号链接文件
S_IFREG 0100000 正常文件
S_IFBLK 0060000 特殊的块设备文件
S_IFDIR 0040000 目录
S_IFCHR 0020000 特殊的字符设备文件
S_IFIFO 0010000 管道文件
S_ISUID 0004000 UID位
S_ISGID 0002000 group-ID位
S_ISVTX 0001000 sticky位
S_IRWXU 00700 利用该掩码来检查属主权限
S_IRUSR 00400 属主有读权限
S_IWUSR 00200 属主有读权限
S_IXUSR 00100 属主有写权限
S_IRWXG 00070 利用该掩码来检查属组权限
S_IRGRP 00040 属组有读权限
S_IWGRP 00020 属组有读权限
S_IXGRP 00010 属组有读权限
S_IRWXO 00007 利用该掩码来检查其他用户权限
S_IROTH 00004 其他用户有读权限
S_IWOTH 00002 其他用户有读权限
S_IXOTH 00001 其他用户有读权限
可以通过位算法来得到具体信息比如:(mode & S_IRWXU)==S_IXUSR来判断是否有执行的权限。另外可以通过下面宏函数进行相应的检查,返回boolean类型:

           S_ISREG(m)  是否为正常文件
S_ISDIR(m) 是否为目录
S_ISCHR(m) 是否为字符设备
S_ISBLK(m) 是否为块设备
S_ISFIFO(m) 是否为管道文件
S_ISLNK(m) 是否为符号链接文件
S_ISSOCK(m) 是否为socket文件

文件访问模式

可以通过系统调用和标准库两种方式打开文件,两种打开i方式分别对应了一些访问模式

系统调用:

O_RDONLY:只读 
O_WRONLY:只写
O_RDWR:读写
O_APPEND:附加在文档后面
O_ASYNC:启动IO的信号变量,
O_CLOEXEC:
O_CREAT:文件创建
O_DIRECT (Since Linux 2.4.10):直接IO,减小缓存对文件内容的影响。但是会影响系统性能。
O_DIRECTORY:不建议使用
O_EXCL :与O_CREAT一起,确定open操作为原子操作。保证文件创建成功,如果两个程序同时创建文件,并且文件已经存在,那么返回错误
O_LARGEFILE
O_NOATIME 
O_NONBLOCK:非阻塞的方式打开
O_NOFOLLOW
O_TRUNC将文件长度设为零,丢弃现有文件

标准库:
r或者rb:只读
w或者wb:只写
a或者ab:追加模式
r+或者rb+或者r+b,以修改的方式打开
w+或者wb+或者w+b,以修改的方式打开,并且设置文件长度为0
a+或者ab+或者a+b,一修改的方式打开,并且内容追家在文件尾部
其中b为二进制文件,否则为字符文件

文件锁

在多个进程在操作同一个文件时,防止操作互相覆盖,需要采用文件锁的机制来解决文件的操作的冲突。linux下面提供了两种锁:锁文件和文件段锁。下面分别介绍一下两种锁

锁文件

多个进程操作同一个文件最直观的方法是:对每一个需要锁的文件创建一个全局的互斥“元素”,只有一个进程“获得”该元素,才可以操作文件,操作结束以后必须释放该元素。本节,我们把该元素叫着“锁文件”。linux的系统调用open函数可以通过创建一个系统唯一的文件来充当锁文件,该操作必须是原子操作,操作完以后,删除该锁文件。实例程序如下:
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int file_desc;
file_desc=open("file.in.lck",O_RDWR|O_CREAT|O_EXCL,S_IWUSR|S_IWGRP|S_IWOTH);
if(file_desc==-1){
printf("wrong\n");
return 0;
}
//锁文件创建好了,下面就是临界区
write(file_desc,"lck",3);
close(file_desc);
unlink("file.in.lck");
//临界区结束
return 1;
}

利用锁文件一般都用来控制诸如串行口之类的资源的独占式方法。但是不适合访问大型的共享文件。如果多个进程只需要操作文件一部分,采用锁文件效率底下。因此才有了文件段锁机制的存在。

文件段锁定

文件段锁定或者文件区锁定指的是通过文件中的锁定区域,实现文件部分锁定。linux提供两种方式fcntl系统调用和lockf调用。lockf调用是fcntl的替代接口,但是由于两者在底层实现机制不同,不允许混合使用两种类型的调用。

#include <unistd.h>
int lockf(int fd, int cmd, off_t len);
其中cmd参数有:
//F_LOCK 设置锁,并且阻塞等待。
//F_TLOCK:设置锁,但是立即返回,如果加锁失败,返回错误
//F_ULOCK解锁指定文件的一部分。
//F_TEST 锁测试

#include <unistd.h>
#include <fcntl.h>
nt fcntl(int fd, int cmd, struct flock *flock_structure);
其中cmd参数有:
F_GETLK:获得文件描述符fd打开的文件的锁信息。它不会尝试的去锁定文件。该函数通过设置flock结构来表明它可能需要的锁类型并定义感兴趣区域。如果调用成功,返回一个非-1值,如果文件已被锁定,应用程序会读取有关信息,并覆盖flock结构,调用应用程序可以检查flock中的l_pid来检查flock是否修改过来判断锁的状态。
F_SETLK:对fd的某个区域进行加锁或解锁。如果成功执行返回-1,该函数是立即返回。
F_SETLKW:和F_SETLK一样,但是会一直等待执行结果。
程序结束以后会自动清楚各种锁
flock结构:
struct flock
{
short int l_type;
short int l_whence;
#ifndef __USE_FILE_OFFSET64
__off_t l_start;
__off_t l_len;
#else
__off64_t l_start;
__off64_t l_len;
#endif
__pid_t l_pid;
};
结构体中l_type 是文件锁定的类型有F_RDLCK共享性读锁定,F_WRLCK独占性写锁定和F_UNLCK释放锁定
成员 l_whence 和lseek类似,有几个可选的参数 SEEK_SET 文件头 SEEK_CUR当前位置SEEK_END文件末尾该字段设定锁定的区域的启始地址
区域的长度 l_len 也就是锁定的长度若该值为0则表示锁定的区域从其起点开始(由l_start和l_whence决定)开始直至最大可能位置为址!该值不能在文件的起始位置之前开始或越过该超始位置。为了锁定整个文件,通常的做法是将l_start说明为0,l_whence说明为SEEK_SET,l_len说明为0

相关函数

系统调用

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int close(int fd);

flags和mode参数见上方相关描述。对于两个进程同时打开了一个文件,会出现操作彼此覆盖问题,解决该问题的方法是文件锁。

ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
两个函数都返回的是实际读写的大小,如果返回-1,表示出现了错误。

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

详细见上面

off_t lseek(int fd, off_t offset, int whence);
设置读写指针, 返回从文件头到当前设置处的偏移值,如果为-1表示出现错误。设置的方法可以相对当前位置,相对结束位置,绝对位置。分别对于whence(SEEK_CUR,SEEK_END,SEEK_SET)。

int dup(int oldfd);
int dup2(int oldfd, int newfd);
文件描述符的复制功能。在管道通信里面,该功能比较好

int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
可以改变文件或者目录的访问权限。

int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
改变用户ID和组ID。

int symlink(const char *oldpath, const char *newpath);
int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);
symlink和link分别创建符号链接和硬链接。unlink删除一个链接,当一个文件的连接数为0,才标识删除相应文件。

【硬连接】
硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

【软连接】
另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。 

int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);
创建和删除目录

int chdir(const char *path);
int fchdir(int fd);
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
char *get_current_dir_name(void);
得到或者改变当前工作目录。

标准函数库

stdio.h
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);
int remove(const char *pathname);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fflush(FILE *stream);

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, fpos_t *pos);

int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int getc(FILE *stream);
int getchar(void);
char *gets(char *s);
int ungetc(int c, FILE *stream);

int fputc(int c, FILE *stream);
int fputs(const char *s, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
int puts(const char *s);

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

dirent.h
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
void seekdir(DIR *dirp, long offset);
long telldir(DIR *dirp);

系统调用和标准库之间的转换

FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);

每一个系统调用的底层描述符和标准库的FILE对象之间都是相关联的。fileno如果失败,返回-1

错误处理

在系统调用和标准库的处理时候,都有各种原因导致错误,失败时,通过设置外部变量errno的值来指明失败的原因。因此程序必须在函数报错之后立即检查errno变量,防止错误被覆盖。man errno可以查到具体的错误代码,其中下面属于比较常见的错误:

EPERM  操作不允许
ENOENT 文件或者目录不存在
EINTR 系统调用被中断
EIO IO错误
EBUSY 设备或资源忙
EEX_IST 文件存在
EINVA_L 无效参数
EMFILE 打开文件过多
ENODEV 设备不存在
EISDIR 是一个目录
ENOTDIR 不是一个目录

上面的错误只是一个代码,可以通过下面的函数使其在说明之间转换

#include <string.h>
#include <errno.h>
extern int errno;
char *strerror(int errnum);
void perror(const char *s);
其中strerror可以在实现转换。perror把error变量中报告的当前错误映射为一个字符串,并把它输出到标准错误输出。输入格式(s:[空格] 具体错误内容)

深入阅读

柳大的Linux讲义·基础篇(1)磁盘与文件系统   http://blog.csdn.net/poechant/article/details/7214673

柳大的Linux讲义·基础篇(2)Linux文件系统的inode http://blog.csdn.net/poechant/article/details/7214926

柳大的Linux讲义·基础篇(3)权限、链接与权限管理 http://blog.csdn.net/poechant/article/details/7215038