目录
文件
文件描述符
内核
对应关系
文件描述符限制
文件描述符唯一性
Socket
什么是进程
输出/入重定向、管道
文件描述符
文件
Linux 一切皆文件
在Linux操作系统中,可以将一切都看作是文件,包括普通文件、目录文件、字符设备文件(键盘、鼠标)、块设备文件(硬盘、光驱)、套接字等等,所有的一切都抽象成文件,并提供了统一的接口,方便应用程序的调用。
文件描述符
平常所谓的打开一个文件,其实都可以称为获取到了文件描述符(File descriptor),简称为fd,当应用程序请求操作系统内核打开/新建一个文件时,内核会返回一个文件描述符用户对标这个被打开/新建的文件,其实fd本质上就是一个非负整数,读写文件都需要使用这个文件描述符来指定待读写的文件。
实际上,文件描述符为一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开/新建一个文件时,内核向进程返回一个文件描述符。不过文件描述符这一概念只适用于UNIX、Linux这样的操作系统。
内核
操作系统为每一个进程维护了一个文件描述符表,该表的索引值都是从0开始的,因此在不同的进程中可以看到相同的文件描述符指向同一个文件,也可能指向不同的文件。

通过上图可以看到,当不同进程中出现相同的文件描述符时,可能实际对应的文件并不是同一个,相反不同进程中不同的文件描述符也可可能对应同一个文件
一般来说,一个进程启动时会从
files[0]
读取输入,将输出写入files[1]
,将错误信息写入files[2]
。(files数组包含该进程打开的文件指针)举个例子,以我们的角度 C 语言的
printf
函数是向命令行打印字符,但是从进程的角度来看,就是向files[1]
写入数据;同理,scanf
函数就是进程试图从files[0]
这个文件中读取数据。每个进程被创建时,
files
的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号
对应关系
通过上面的关系图,可以得到以下的关系:
进程拿到文件的文件描述符ID等于拿到进程文件描述符表的索引 ==》通过索引拿到文件指针,指向了系统级文件描述符表的文件偏移量 ==》再通过文件偏移量找到inode指针,最终对应到真实的文件
文件描述符限制
文件描述符是十分重要的系统资源,理论上系统内存多大就可以打开多少个文件描述符,但实际情况是内核会有系统级/用户级限制,这是为了防止某个进程消耗掉所有的文件资源,可以通过 ulimit -n 查看,一般是65535
文件描述符唯一性
进程 + 文件描述符ID确认,因为内核为每一个进程都维护了一份其所属的文件描述符表
Socket
Socket也叫套接字,它也是文件。当Server端监听到有连接时,应用程序会请求内核创建Socket,Socket创建好后会返回一个文件描述符给应用程序,当有数据包经过网卡时,内核会针对不同的传输方式(TCP、UDP)通过数据包的源IP、源端口、目地IP、目地端口(即四元组)等信息在内核维护的一个ipcb双向链表中找到对应的Socket,并将数据包赋值到该Socket的缓冲区,应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成对Socket数据的读取。
进程
什么是进程
抽象地说,计算机可以看成是这样的:

整个内存空间被划分为用户空间和内核空间。
用户空间:装载着用户进程需要使用的资源
内核空间:存放着内核进程需要加载的系统资源,一般情况下这些资源用户是不允许访问的。但是特别情况进程会共享一些内核空间的资源,例如动态链接库等等。
当我们用go写一个helloword后,经过编译可以得到一个可执行文件,在命令行运行就可以打印出一句hello word,然后程序退出。在操作系统层面,就是新建了一个进程,然后该进程将经过编译后的可执行文件读入内存空间,然后执行,最后退出。编译好后的可执行文件仅仅是一个文件,并不是进程,可执行文件必须载入内存,包装成一个进程才能真正的跑去来。进程是依靠操作系统创建的,每个进程都有固定的属性,比如进程号(PID)、进程状态、打开的文件等等,进程创建好之后,读入你的程序,你的程序才被系统执行
Linux进程数据结构:
// Linux源码
struct task_struct {
// 进程状态
long state;
// 虚拟内存结构体
struct mm_struct *mm;
// 进程号
pid_t pid;
// 指向父进程的指针
struct task_struct __rcu *parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct *fs;
// 一个数组,包含该进程打开的文件指针
struct files_struct *files;
......
};复制
其中task_struct
就是Linux内核对于一个进程的描述,也可以称为进程描述符。mm
指向的是进程的虚拟内存,files
指针指向一个数组,这个数组里装着所有该进程打开文件的指针,可以通过以下命令查看当前进程所包含的文件。
ls /proc/$$/fd
复制
输出/入重定向、管道
再来看一个图

如果看了上面文件描述符的讲解,就可以很好理解输出/入重定向、管道的操作了。
输入重定向
$ command < file.txt
复制
只需将file[0]指向一个文件,那么进程就会从这个文件中读取数据,而不是从外设键盘
输出重定向
$ command > file.txt
复制
只需将file[1]指向一个文件,那么进程输出就不会写入到显示器,而是写入到这个文件。
管道
$ cmd1 | cmd2 | cmd3
复制
管道符其实就是将一个进程的输出流和另一个进程输入流连接起来形成一条「管道」
线程
以下的内容是基于Linux的,不同操作系统进程和线程的内容不同。
从Linux内核的角度来说,进程和线程没有被区别对待,为什么这么说呢?因为无论是进程还是线程,都是用上面提到的task_struct
结构来表示,它们唯一的区别就在于共享的数据区域不同,而通过fork()
创建的进程和父进程之间的数据内容是拷贝而不是像线程之间的共享。
正式因为线程是共享了进程的数据,所以在多线程程序中才需要利用锁机制,避免多个线程同时往同一区域对数据进行操作导致数据的错乱。
Refer
https://www.jianshu.com/p/ededa453b2b0 https://www.jianshu.com/p/a2df1d402b4d https://en.wikipedia.org/wiki/Procfs