2021-01-08 Linux I/O 操作

225 阅读11分钟

Linux 系统中,外存中的数据都是以文件的形式保存的,对目录和各种设备的操作也等同于文件的操作。

1 Linux 系统文件和文件系统

1.1 Linux 文件类型(ls -l)

表1.1.1 - 各种文件类型含义

文件类型标识
普通文件第 1 个字符为 -
目录文件第 1 个字符为 d
硬链接文件除了显示的文件数量,其他都和某个普通文件一模一样的文件
软链接文件第 1 个字符为 l
块设备文件第 1 个字符为 b
socket第 1 个字符为 s
字符设备文件第 1 个字符为 c
管道文件第 1 个字符为 p
setUid 可执行文件第 4 个字符为 s
setGid 可执行文件第 7 个字符为 s
setUid 加 setGid 文件第 4 个和第 7 个字符都是 l

清单1.1.2 - C 语言获取 Linux 文件类型示例

#inclue <stdlib.h>


// 执行成功则返回 shell 命令的值;
// 调用/bin/sh 失败返回127;
// 其他失败原因返回-1;
// 参数string为NULLL,则返回非零值
int res = system("ls -l");

1.2 Linux 文件权限(ls -l)

清单1.2.1 将 /etc/passwd 文件设置为文件所有者可读可写,其他用户为只读权限

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


// 权限更改成功返回0;
// 失败返回-1;
// 错误原因存于errno
chmod("/etc/paddwd", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);

权限参数(mode)的组成方式:S_I``R/W/X``USR/GRP/OTH
其中 USR=所有者,GRP=组,OTH=其他用户;R=读,W=写,X=执行


通过设置权限掩码(umask),可以设置系统文件在创建时的初始权限

清单1.2.2 umask 函数示例

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    system("touch old_umask_file"); // 创建文件

    mode_t new_umask = 0666; // 八进制的666
    mode_t old_umask = umask(new_umask); // 设置系统权限掩码为0666,返回系统原来的权限掩码

    system("touch 0666_umask_file"); // 设置过系统权限掩码后,创建的文件
    system("mkdir 0666_umask_dir"); // 设置过系统权限掩码后,创建的文件夹
}


/*
ls-l
d--x--x--x 2 zhaoxuyang03 zhaoxuyang03  4096 1月   8 22:50 0666_umask_dir
---------- 1 zhaoxuyang03 zhaoxuyang03     0 1月   8 22:52 0666_umask_file
*/

在 Linux 系统中,定义了 stat 结构来存放文件属性

struct stat {
	dev_t st_dev; // 文件所在设备的 ID
	ino_t st_ino; // 索引节点号
	mode_t st_mode; // 文件保护模式
	nlink_t st_nlink; // 文件的链接数(硬链接)	
	uid_t st_uid; // 用户 ID
	gid_t st_gid; // 组 ID
	dev_t st_rdev; // 设备号,针对设备文件
	off_t st_size; // 文件字节数
	unsigned long st_blksize; // 系统块的大小
	unsigned long st_blocks; // 文件所占块数
	time_t st_atime; // 最后一次访问时间(access)
	time_t st_mtime; // 最后一次修改时间(modify)
	time_t st_ctime; // 最后一次改变时间(指属性)
};

下例展示了如何获取文件大小

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


int main() {
	struct stat buf;
	
	stat("/etc/passwd", &buf);
	printf("%d\n", buf.st_size);
	return 0;
}

2 不带缓存的文件 I/O 操作 (File)

不带缓存的文件 I/O 操作,包括系统调用或 API 的 I/O 操作,是由操作系统提供的,符合 POSIX 标准,但是不能移植到非 POSIX 标准的操作系统(如 Windows)。主要用到以下操作:

函数作用
creat创建文件
open打开或创建文件
close关闭文件
read读文件
write写文件
lseek移动文件的读写位置
flock锁定文件或解除锁定(用于文件加建议锁)
fcntl文件描述符操作(用于文件加强制锁)

2.1 文件的创建 (creat)

creat 函数用于创建文件,成功则由内核返回一个最小可用的文件描述符,若有错误发生则会返回-1

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int creat(const char* pathname, mode_t mode);

2.2 文件的打开和关闭 (open & close)

  • open 函数用于打开一个存在或不存在的文件,成功则由内核返回一个最小可用的文件描述符,若有错误发生则会返回-1
  • close 函数用于关闭一个打开的文件,成功关闭则返回0,发生错误时返回-1;当一个进程终止时,它所有已打开的文件都由内核自动关闭
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
int close(int fd);
flags 值参数说明
O_RDONLY以只读模式打开
O_WRONLY以写入模式打开
O_RDWR以读写模式打开
O_APPEND在文件尾写入数据
O_TRUNC设置文件的长度为0,并舍弃现存的数据
O_CREAT创建文件,可使用 mode 参数设置访问权限
O_EXCL与 O_CREAT 一起使用,如果要创建的文件已存在,则打开失败

2.3 文件的读写操作 (write & read)

  • read 函数用于从指定的文件描述符中读出数据

    • 常规文件的读写不会阻塞,除非从网络或控制台等场景读入
  • write 函数用于向打开的文件写入数据,写操作从文件当前位置开始

  • lseek 函数用于在指定的文件描述符中将文件指针定位到相应的位置

#include <unistd.h>


ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void* buf, size_t count);

2.4 文件的非阻塞操作 (O_NONBLOCK)

  • 文件打开时用 flags 中的 O_NONBLOCK 来表示 write/read 是非阻塞的
  • 非阻塞与阻塞的区别在于没有数据到达的时候是否立刻返回
  • 如果设备暂时没有数据可读就返回-1,同时置 errno 为 EWOULDBLOCK
  • 非阻塞主要用在网络服务中,使得服务器得到最大的利用
非阻塞时设置 flags |= O_NONBLOCK
阻塞时设置 flags &= ~O_NONBLOCK


非阻塞程序结构如下:


while(1) {
	非阻塞read(设备1);
	if(设备1有数据到达) {
		处理数据;
	}
	非阻塞read(设备2);
	if(设备2有数据到达) {
		处理数据;
	}
	......
}

2.5 文件上锁 (flock & fcntl)

  • flock 用于给文件加建议锁,一般情况下,系统很少使用建议锁
  • fcntl 用于给文件加强制锁,内核将阻止其他任何文件对其进行读写操作
  • 通过 F_GETFL, F_SETFL 分别用于读取、设置文件的属性,能够更改的文件标志有 O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, O_NONBLOCK
// 1. 获取文件的 flags
flags = fcntl(fd, F_GETFL, 0);


// 2. 增加文件的某个 flgas,例如将阻塞设置为非阻塞
flags |= O_NONBLOCK;


// 3. 设置文件的 flags
fcntl(fd, F_SETFL, flags);

struct flock {
	short l_type; // 加锁的类型:F_RDLCK, F_WRLCK, F_UNLCK
	short l_whence; // 对 l_start 的解释,分别为 SEEK_SET, SEEK_CUR, SEEK_END
	off_t l_start; // 指明加锁部分的开始位置
	off_t l_len; // 加锁的长度
	pid_t l_pid; // 加锁进程的进程id
}
#include <sys/file.h>


/*
operation有四种情况:
- LOCK_SH:建立共享锁定,多个进程可同时对同一个文件作共享锁定
- LOCK_EX:建立互斥锁定,一个文件同时只有一个互斥锁定
- LOCK_UN:解除文件锁定状态
- LOCK_NB:无法建立锁定时,此操作可不被阻断,马上返回进程,通常与 LOCK_SH 或 LOCK_EX 做 OR 组合


单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定
*/
int flock(int fd, int operation);


返回0表示成功,若有错误则返回-1,错误代码存于errno
#include <unistd.h>
#include <fcntl.h>


int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock* lock);


返回0表示成功,若有错误则返回-1,错误代码存于errno

3 带缓存的流文件 I/O 操作 (InputStraem & OutputStream)

带缓存的流文件 I/O 操作,又称标准 I/O 操作,符合 ANSI C 标准,在内存中开辟缓冲区,为程序的每一个文件使用,比不带缓存的文件 I/O 程序方便移植;主要用到以下操作:

函数作用
fopen打开或创建文件
fclose关闭文件
fgetc由文件中读取一个字符
fputc将一指定字符写入文件流中
fputs将一指定的字符串写入文件流中
fread从文件流成块读取数据
fwrite将数据成块写入文件流
fseek移动文件流的读写位置
rewind重设文件流的读写位置到文件开头
ftell取得文件流的读取位置

3.1 流文件的打开和关闭 (fopen & fclose)

  • fopen 函数用于打开文件
  • fclose 函数用于关闭文件
#include <stdio.h>


FILE *fp;
if ((fp=fopen("xxx", "a+")) == NULL) {
	exit(0);
}
fclose(fp);
FILE* fopen(const char*path, const char* mode);
- r 表示打开只读文件。该文件必须存在
- r+ 表示打开可读写的文件。该文件必须存在
- w 表示打开只写文件。若文件存在则文件长度清为零,即**该文件内容会清空**;若文件不存在则创建该文件
- w+ 表示打开可读写文件。若文件存在则文件长度清为零,即**该文件内容会清空**;若文件不存在则创建该文件
- a 表示以附加方式打开只写文件。若文件不存在,则会建立该文件;所文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留
- a+ 表示以附加的方式打开可读写的文件。若文件不存在,则会建立该文件;所文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留
- 上述形态都可以再添加一个字符'b',表示打开二进制文件(POSIX 系统的 Linux 会忽略 b 字符);新建文件会有 S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)权限


int fclose(FILE* stream);
成功返回0,失败返回EOF

3.2 流文件的读写操作 (fget & fputc & fputs & fwrite & fread)

#include <stdio.h>


int fget(FILE* stream); // 成功返回读取的字符,失败返回EOF


int fputc(int c, FILE* stream); // 成功返回写入的字符,失败返回EOF


int fputs(const char* s, FILE* stream); // 成功返回写出的字符个数,失败返回EOF


/*
- ptr:将写入的数据地址
- size:字符串长度
- nmemb:字符串数目
- stream:文件流
*/
size_f fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream); // 成功返回实际写入的 nmemb 数目,失败返回 EOF


size_t fread(const void* ptr, size_t size, size_t nmemb, FILE* stream); // 成功返回实际读取到的 nmemb 数目,失败返回 EOF

3.3 文件的定位 (fseek & ftell & rewind)

/*
移动文件流的读写位置
stream 为文件流
whence 有以下几种:
- SEEK_SET 从距文件开头 offset 位移量为新的读写位置
- SEEK_CUR 以目前的读写位置往后增加 offset(允许负值) 个位移量
- SEEK_END 将读写位置指向文件尾后再增加 offset(允许负值) 个位移量
*/
int fseek(FILE* stream, long offset, int whence); // 调用成功返回0,有错误返回-1

// 取得文件流的读取位置
long ftell(FILE* stream); // 成功返回当前读写位置,有错误返回-1


// 重设文件流的读写位置为文件开头
void rewind(FILE* stream);

4 特殊文件的操作

函数作用
opendir打开目录文件
readdir读取目录文件
closedir关闭目录文件
symlink建立软链接
link建立硬链接

4.1 目录文件的操作 (opendir & readdir & closedir)

struct __dirstream {
	void *__fd; // hurd 指针
	char *__data; // 文件夹块
	int __entry_data;
	char *_ptr; // 当前在块中的指针
	int __entry_ptr;
	size_t __allocation;
	size_t __size;
	__libc_lock_define (, __lock);
};
typedef struct __dirstream DIR;


struct dirent {
	ino_t d_ino; // 此目录进入点的 inode 的节点号
	ff_t d_off; // 目录文件开头至此目录进入点的位移
	signed short int d_reclen;
	unsigned char d_type; // 所指的文件类型
	har d_name[256]; // 文件名,不包含 NULL 字符
};
#include <sys/types.h>
#include <dirent.h>
DIR* opendir(const char* name); // 成功返回目录流,失败返回NULL


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct dirent * readdir(DIR* dir); // 成功则返回下个目录进入点,有错误发生或读取到目录文件尾则返回NULL


#include <sys/types.h>
#include <dirent.h>
int closedir(DIR * dir); // 关闭成功返回0,失败返回-1,错误原因存于errno中

4.2 链接文件的操作 (symlink & link)

软链接(符号链接)硬链接
跨越不同文件系统不支持跨越不同文件系统(例如/bin目录和用户目录属于不同的文件系统)
可以在目录间建立不可以给目录建立硬链接
如果链接指向的文件从一个目录移动到另一个目录,就无法通过软链接访问(含有源文件在文件结构中的路径信息)
需要一个索引节点,需要占用空间
软连接文件和源文件是不同类型的文件,也是不同的文件,inode号也不同具有相同 inode 的文件互为硬链接文件
删除源文件,链接文件依然存在,但是无法指向源文件删除硬链接文件或者删除源文件任意一个,文件数据实际并未删除;只有删除源文件以及所对应的所有硬链接文件,文件数据才被删除,同时释放磁盘空间
#include <unistd.h>

/*
建立软链接
- oldpath 已存在文件路径和文件名(一定要存在,否则不生效)
- newpath 链接的名称
*/
int symlink(const char* oldpath, const char *newpath); // 成功返回0,失败返回-1,错误原因存于errno

// 建立硬链接
int link(consta char* oldpath, const char *newpath); // 成功返回0,失败返回-1,错误原因存于errno