一、引言
文件操作是Linux C编程中其中的一项核心技术,实际上也相当重要,这里并不是说狭义上的那种文件操作,它也非常有助于理解和学习Linux系统。为什么这样说呢?因为在Unix/Linux的世界中,一切皆文件!这种简单的设计其实非常有利于编程设计,让你可以集中在数据结构和算法的设计上,例如网络Socket和外部设备都作为文件处理,省去了很多繁琐的概念。
本文主要根据Linux文件实质原理展开讨论,其中也涉及到Linux的进程(Process)原理分析,Linux系统的进程对象持有一个文件记录表,该文件记录表保存一个该进程可处理的文件句柄数组,那么什么是文件描述符呢?它其实就是一个索引,就是文件句柄数组的下标索引,引用维基百科:
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
维基百科
这里讨论的都是广泛意义上的文件,你不需要强加想象将什么当做文件,下面会有具体的讲解。
二、进程管理与原理分析
什么是Linux进程?Linux进程是系统执行的一个程序以及该程序所管理的资源,在内存中执行一个简单的helloworld应用程序,这是一个进程,通常我们会说一个应用程序就是一个进程,严格来说并不正确,因为一个应用程序可以有多个子进程。
那么如何才能理解Linux的进程概念呢?从编程设计的角度来看,它应该被设计为一个结构体,C语言基本就是指针和结构体了,而结构体是最基本的数据结构单元。
我们一起来分析一下Linux内核源码,这里用的是linux2.6.0,立即吃瓜:https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/。
Linux进程的源码文件为/include/linux/sched.h,取感兴趣的数据成员,进程结构体对象为:
struct task_struct {
// 进程可运行状态,-1不可执行,0可执行,>0可终止
volatile long state;
// 线程信息,在这里你可以到线程是从属于进程的
struct thread_info *thread_info;
// 进程描述符使用计数,表示该进程是否在被使用
atomic_t usage;
// 内核对每个进程的状态标识,如PF_FORKNOEXEC表示进程刚创建但未执行
unsigned long flags;
// 进程的调度策略,SCHED_FIFO和SCHED_RR为实时进程,SCHED_OTHER未分时进程
unsigned long policy;
// 进程标识符,代表一个进程
pid_t pid;
// 进程组标识符,代表该进程所属进程组别
pid_t __pgrp;
// 以下表示进程间的从属关系
struct task_struct *real_parent; /* real parent process (when being debugged) */
struct task_struct *parent; /* parent process */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
// 进程的CPU状态信息
struct thread_struct thread;
// 文件系统信息
struct fs_struct *fs;
// 该进程打开的文件信息
struct files_struct *files;
// namespace
struct namespace *namespace;
};
以上代码,取了进程的一些基本信息,如进程ID,然后你会发现线程是属于进程的,但是Linux的进程和线程有什么区别呢?首先是从属关系,然后进程和线程的本质区别是,进程占据单独的内存空间,包括全局静态区、文字常量区、堆区、栈区和代码区,而线程是单独占据一个栈区,所以多线程是处理多个栈区包括主线程栈区。
接着还有进程之间的关系,子进程和父进程等,这里就表明了一个应用程序是可以有多个进程的。
最后红色加粗的struct
files_struct *files这个结构体变量表示该进程所打开的文件信息,该文件在/include/linux/file.h中:
// 打开文件表结构
struct files_struct {
// 使用该表的进程数
atomic_t count;
spinlock_t file_lock; /* Protects all the below members. Nests inside tsk->alloc_lock */
int max_fds;
int max_fdset;
int next_fd;
// 当前文件描述符数组的指针,下面fd_array的指针
struct file ** fd;
fd_set *close_on_exec;
fd_set *open_fds;
fd_set close_on_exec_init;
fd_set open_fds_init;
// 一个文件结构体指针数组,表示文件描述符数组,保存打开的文件
struct file * fd_array[NR_OPEN_DEFAULT];
};
每个进程都有这个文件描述符表,我们操作的文件对象都在fd_array中,文件描述符就是这个数组的索引,发现这样称呼也不甚准确,如果称索引为文件描述符,那么文件对象称指针会好点,不过看懂了就不用纠结了。你可以发现函数read和write,或者socket中的send和recv等,这些都是使用文件描述符来进行操作的,如果不明白其中的原理,OOP编程入门的开发者可能会想:为什么传个int就可以进行网络传输了?
三、Linux文件原理分析
这里要注意,该文件描述符数组在进程创建的时候默按顺序认初始化3个文件句柄:stdin,stdout,stderr,用于标准输入,标准输出,标准错误输出。
到这里你可以知道,对文件操作的基本元素有:文件句柄/指针,和文件描述符(索引),fdopen()函数可以将文件描述符转为文件指针,你可以想到的实现就是,根据索引从数组中获取到一个文件指针。
接着,我们需要扩展文件的概念,在linux中一切皆文件,这会很大程度上影响我们自己的编程想法,那么文件是什么样子的呢?看内核/include/linux/fs.h文件结构体的代码:
struct file {
struct list_head f_list;
// 目录结构
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
// 文件操作,文件相关的函数操作,使用一个结构体封装函数指针
struct file_operations *f_op;
atomic_t f_count;
// 文件标志
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
int f_error;
struct file_ra_state f_ra;
unsigned long f_version;
void *f_security;
/* needed for tty driver, and maybe others */
void *private_data;
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
};
struct file和FILE是一样的,都代表一个文件,说了那么多,到底在Linux中具体哪些都是文件呢?
- 普通文件File,如一个txt文本,一个视频文件,一个字节码文件;
- 终端I/O,如字符终端;
- 管道Pipe,如普通管道和FIFO;
- 网络通信资源,套接字socket;
- 设备Device,网络设备,硬盘块设备,块设备。
以上都是我们常见的Linux文件,进一步我们可以将文件看作一种资源池,我们主要是对资源池中的数据进行读取,但是我们不说文件都是二进制表示所以说一切都是文件,基本是废话,二进制太广泛了,主要还是对数据结构和算法的理解,上面说的都是数据结构,实际相应的头文件中定义了相关操作。