Linux C编程详解(进程原理分析、文件描述符和文件记录表、文件句柄和文件原理)

一、引言文件操作是Linux C编程中其中的一项核心技术,实际上也相当重要,这里并不是说狭义上的那种文件操作,它也非常有助于理解和学习Linux系统。为什么这样说呢?因为在Unix/Linux的世界中,一切皆文件!这种简单的设计其实非常有利于编程设计,让你可以集中在数据结构和算法的设计上,例如网络Socket和外部设备都作为文件处理,省去了很多繁琐的概念。

Linux C编程详解(进程原理分析、文件描述符和文件记录表、文件句柄和文件原理)

文章图片
本文主要根据Linux文件实质原理展开讨论,其中也涉及到Linux的进程(Process)原理分析,Linux系统的进程对象持有一个文件记录表,该文件记录表保存一个该进程可处理的文件句柄数组,那么什么是文件描述符呢?它其实就是一个索引,就是文件句柄数组的下标索引,引用维基百科:
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
维基百科
这里讨论的都是广泛意义上的文件,你不需要强加想象将什么当做文件,下面会有具体的讲解。
二、进程管理与原理分析什么是Linux进程?Linux进程是系统执行的一个程序以及该程序所管理的资源,在内存中执行一个简单的helloworld应用程序,这是一个进程,通常我们会说一个应用程序就是一个进程,严格来说并不正确,因为一个应用程序可以有多个子进程。
Linux C编程详解(进程原理分析、文件描述符和文件记录表、文件句柄和文件原理)

文章图片
那么如何才能理解Linux的进程概念呢?从编程设计的角度来看,它应该被设计为一个结构体,C语言基本就是指针和结构体了,而结构体是最基本的数据结构单元。
我们一起来分析一下Linux内核源码,这里用的是linux2.6.0,下载地址:https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/。
【Linux C编程详解(进程原理分析、文件描述符和文件记录表、文件句柄和文件原理)】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,用于标准输入,标准输出,标准错误输出。
Linux C编程详解(进程原理分析、文件描述符和文件记录表、文件句柄和文件原理)

文章图片
到这里你可以知道,对文件操作的基本元素有:文件句柄/指针,和文件描述符(索引),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_tf_count; // 文件标志 unsigned intf_flags; mode_tf_mode; loff_tf_pos; struct fown_struct f_owner; unsigned intf_uid, f_gid; intf_error; struct file_ra_state f_ra; unsigned longf_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_tf_ep_lock; };

struct file和FILE是一样的,都代表一个文件,说了那么多,到底在Linux中具体哪些都是文件呢?
  • 普通文件File,如一个txt文本,一个视频文件,一个字节码文件;
  • 终端I/O,如字符终端;
  • 管道Pipe,如普通管道和FIFO;
  • 网络通信资源,套接字socket;
  • 设备Device,网络设备,硬盘块设备,块设备。
以上都是我们常见的Linux文件,进一步我们可以将文件看作一种资源池,我们主要是对资源池中的数据进行读取,但是我们不说文件都是二进制表示所以说一切都是文件,基本是废话,二进制太广泛了,主要还是对数据结构和算法的理解,上面说的都是数据结构,实际相应的头文件中定义了相关操作。

    推荐阅读