【Linux简介】
操作系统启动过程
计算机启动后CPU从默认的地址处读取 NOR Flash 存储器中的固件执行,固件检测计算机各种设备工作正常后,去辅存查询下一个程序执行,这个程序就是操作系统启动入口、或者操作系统安装程序执行入口,查询方式有如下两种。
Legacy 方式
此方式在辅存的第一个扇区或第一个页开始查询,查询长度为512字节,若此段数据的末尾为0x55和0xaa,则认为这段数据是操作系统启动入口程序,也称为操作系统引导程序,之后将其读取到内存中执行,若按顺序查询的第一个辅存没有引导程序,则去下一个辅存中查询,若所有辅存都没有发现引导程序,则无法继续执行。
使用Legacy方式的计算机安装多个操作系统时,需要在第一个引导程序之后再分出多个引导,分别引导不同的操作系统启动,主引导程序损坏后其它分支引导程序都将无法执行,所有的操作系统都将无法启动。
UEFI 方式
此方式需要首先在辅存中创建一个FAT32分区,之后将操作系统引导程序以及相关属性数据放在此分区中,固件会在辅存中查询FAT32分区并读取其中的引导程序执行,从而启动操作系统。
使用UEFI方式的计算机安装多个操作系统时,每个引导程序互不干扰,任何一个引导程序损坏都不影响其它操作系统启动。
Linux与GNU
linux是一种操作系统内核,内核是操作系统最重要、最复杂的部分,但不是操作系统全部的组件,linux内核经常与GNU项目组件组成操作系统,GNU项目的目的是研发一组开放源代码的基础软件,包括操作系统、编译器、常用基础程序,因为GNU项目的操作系统内核研发迟缓,所以现在的开源操作系统多数使用linux + GNU组件组成,使用linux内核的操作系统称为linux发行版。
与Linux内核组成操作系统的基础GNU组件如下:
1.GCC,高级语言编译器,主要用于编译C、C++代码。
2.Binutils,程序开发工具集,包括汇编器、连接器、其它相关工具。
3.Glibc,一组链接库文件,用于将系统调用封装为C语言函数,方便高级编程语言像使用C语言函数一样使用系统调用,C语言标准函数库、C++标准库、操作系统API都会使用Glibc。
4.GTK,用于制作程序图形界面,同时非GNU项目的QT也广泛流行。
5.Grub,多操作系统启动管理工具。
【系统调用】
操作系统最主要的作用是管理其它程序运行,另一个重要作用是提供程序常用功能,比如:读写文件、创建进程、申请内存、网络通信,操作系统对程序提供的这些功能称为系统调用,系统调用可以被程序调用执行。
系统调用的执行方式与C语言函数类似,执行时也需要输入参数,每个系统调用都有一个编号,比如读取文件的系统调用编号为0、写入文件的系统调用编号为1,用户程序使用此编号指定需要使用的系统调用。
用户程序使用的指令权限低于系统内核,无法使用跳转指令直接执行系统调用,用户程序使用系统调用是通过中断实现的,首先用户程序发出一个编号为0x80的内中断,系统内核接收到中断后,根据寄存器中存储的系统调用编号和参数确定执行哪个系统调用,以及如何执行,可以认为系统调用就是系统内核提供的一组中断处理程序。
使用系统调用的具体步骤如下:
1.将系统调用的编号写入rax寄存器。
2.将系统调用的参数写入函数传参寄存器,比如写入文件系统调用有三个参数,使用rdi、rsi、rdx寄存器存储。
3.使用 int 0x80(x86处理器) 或 syscall(x86-64处理器) 指令发出内中断。
4.系统内核接收到内中断后,根据寄存器中的数据执行用户程序需要使用的功能。
5.系统调用执行完毕后,执行恢复现场指令,之后返回用户程序执行。
这里使用C语言内嵌汇编代码的方式说明系统调用提供的终端输出功能如何使用。
#include <stdio.h>
char ali[] = "阿狸\n";
char * p = &ali[0];
int main()
{
__asm__
(
"\
mov rax, 1; \
mov rdi, 1; \
mov rsi, p; \
mov rdx, 7; \
syscall; \
"
);
return 0;
}
rax设置系统调用编号,这里使用写入文件系统调用。
rdi设置文件描述符编号,1为标准输出文件。
rsi设置写入字符串的地址。
rdx设置写入字符串的长度,单位字节,不包含末尾空字符。
syscall指令发出0x80号内中断。
【内存管理】
内存碎片
多个已用存储单元中间的未用存储单元称为存储碎片,这些零碎的存储单元不方便使用,比如有10个存储单元,首先存储如下长度的三个数据:1字节、2字节、3字节,依次排列在地址0-5的存储单元中,之后删除长度2字节的数据,中间空余2个存储单元,若要再存储一个4字节数据,此数据只能存储于地址6-9的存储单元中,地址1-2的存储单元无法使用。
内存是按页使用的,内存碎片分为两种:页内碎片、页间碎片,linux的主要工作是防止页间碎片过多产生,为此linux会将不常用的页间碎片集中在一起并移动到空闲区域,从而清理碎片。
进程申请内存
执行程序时操作系统会分配其所需的全部内存空间,但是有些进程需要在执行期间临时向操作系统申请内存使用,linux有多种临时分配内存的策略:
1.分配不连续的内存。
2.分配全部连续的内存。
3.按页分配物理内存。
进程虚拟地址空间
程序在内存中执行时使用虚拟地址指定要操作的内存,虚拟地址等于程序在内存中的文件内部地址,因为内存碎片的原因操作系统会为进程分配不连续的内存页,并使用页表记录进程占用了哪些页、以及虚拟地址对应的内存物理地址,处理器执行指令时根据页表将虚拟地址转换为物理地址,虽然程序在内存中不是连续存储的,但是程序的虚拟地址不会改变,这不会产生影响,就像为班级中的学生按位置分配数字编号,即使之后这些学生混合在不同宿舍中,只要记录了每个学生的宿舍编号就可以查询到对应编号的学生。
虚拟地址与物理地址都使用IP寄存器指定,一个程序可用的虚拟地址空间容量与计算机可用的物理内存空间容量是相同的,理论上一个程序可以使用整个物理内存。
x86-64处理器的IP寄存器长度为64位,使用一个64位的二进制数据可以形成非常大的内存地址空间,但是实际上个人用户远用不到这么大的内存地址,所以某些处理器只使用其中的48位,这样可以简化虚拟地址的转换步骤,IP寄存器剩余的高16位有两种状态,系统内核指令将其全部设置为1(只是单纯设置为1,不参与页表转换),用户程序指令将其全部设置为0,若不是这两种状态则是不规范的虚拟地址,处理器无法使用。
程序指令使用的虚拟地址从0x400000开始分配,而非从0开始,0x400000表示程序执行时在内存中的第一个数据,之所以这样规定完全是历史原因,没有特殊优势,就像中国高铁的轨距为什么是1435毫米一样。
虚拟内存
当计算机剩余可用内存不足时,操作系统会将长时间不使用的内存数据放到外存中,从而腾出一些内存空间,当需要使用这些数据时重复之前的步骤,之后将数据从外存读取到内存,这种技术称为虚拟内存,使用虚拟内存技术可以让内存容量小的计算机正常运行大型程序。
虚拟内存与虚拟地址是不同的概念,不要搞混,虚拟内存是使用外存模拟出来的内存,而虚拟地址是程序在内存中执行时使用的文件内部地址,实际上不同的人有不同的理解方式,也就有不同的名词解释方式,计算机知识很多名词都没有统一的解释,新手很容易越听越迷糊。
【进程管理】
linux内核启动后,初始代码以实模式运行,内存以分段方式使用,之后启动保护模式,并创建内存管理等一些基础功能,之后创建如下两个进程:
1.init进程,用于启动用户进程。
2.kthreadd进程,用于启动系统内核进程。
进程与线程
在内存中执行的程序称为进程,早期的进程,内部代码只能单线执行,程序实现的多个功能需要依次排队执行,随着进程的功能越来越多,有些功能需要同时执行,此时就需要将需要同时执行的代码分组存储,每组称为一个线程,每个线程都有独立的执行权、可以与其它线程同时执行。
但是注意,线程并不是把进程内所有数据全部分组,而是只将需要独立执行的函数、全局数据分组,未分组的数据直属进程,所有线程都能使用,程序以线程为单位执行,操作系统执行的是线程而非进程,每个线程都有自己的栈空间,直属进程的函数不能自己执行,需要被其它线程调用才能执行,此时其使用调用者的栈空间。
main函数是程序的主线程,若没有创建其它线程,则程序就只有这一个线程。
进程属性
进程编号
每个进程有一个唯一的数字编号,英文简称pid,操作系统根据进程编号管理进程,创建进程时操作系统将现在最大进程编号+1分配给新进程,若有进程终止执行则操作系统回收进程编号,当进程编号分配到最大值后操作系统会从300开始重新检查空余编号分配给新进程,从300开始查询是因为最先启动的系统内核进程、守护进程会长期执行,他们一般占用300之前的进程编号。
进程执行状态
进程主要分为以下几种执行状态:
1.准备就绪状态,新进程创建后默认处于此状态,此时进程还没有运行。
2.运行状态,进程正在执行,内核将此状态与准备就绪状态记录为同一种状态,内核并不会区分准备就绪与运行状态。
3.轻度睡眠状态,进程暂停执行,可以被信号恢复执行。
4.中度睡眠状态,进程暂停执行,只能被重要的信号恢复执行。
5.深度睡眠状态,进程暂停执行,不能被信号恢复或者终止执行,不对信号做任何反应,只能由操作系统重新调用它执行。
6.跟踪状态,进程由调试器设置为暂停状态,可由调试器恢复执行。
7.挂起状态,也称停止状态,进程暂停执行,并且短期内不会恢复执行,操作系统将进程数据从内存转移到外存,进程需要执行时再读取到内存。进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号后会进入此状态,向进程发送SIGCONT信号后将会从挂起状态恢复执行。
8.僵尸状态,进程终止执行,进程数据在内存中删除,但是保留进程描述符,其中存储了进程基础属性信息、进程终止相关信息,用于供父进程查询使用,若父进程设置了子进程退出事件,则此时父进程会收到子进程终止的信号通知。
9.死亡状态,进程描述符从内存删除,内存中不再有此进程的任何信息。
其中暂停状态比较复杂,此状态又会细分为多种,不同的暂停状态恢复方式也不同。
内核进程与用户进程
系统内核创建的进程称为内核进程,使用最高级别指令权限。
系统用户创建的进程称为用户进程,使用最低级别指令权限。
内核态与用户态
用户进程可以使用系统调用实现自己需要的功能,进程使用的系统调用会算入用户进程的一部分,系统调用的执行时间也会算入用户进程执行的总时间。
当用户进程执行系统调用时,此时进程属于内核状态,简称内核态,当用户进程执行自己的代码时,此时进程属于用户状态,简称用户态。
内核栈与用户栈
进程执行时操作系统以线程为单位分配栈空间,称为用户栈,进程使用系统调用时,操作系统会为系统调用分配一个专用的栈空间,这个栈空间称为内核栈,系统调用不会使用调用者的用户栈。
同一个系统调用可以被多个进程同时使用,系统调用每次执行时都会分配一个独立的内核栈,所以系统调用同时执行多次时不会产生冲突。
进程描述符
在计算机程序知识中,经常有人使用“符号”表示“名称”,很多人将“数据名”称为“符号”,这也许是因为某些英文直译的结果,但是这种称呼并不合适,甚至很容易让新手产生误解。
很多英文单词会被统一翻译为描述符,描述符指的是一个用于存储各种属性信息的结构体,比如进程描述符存储进程相关属性,常用属性有:进程名称、进程编号、进程执行状态、父进程、子进程、进程调度策略和优先级、打开文件列表。
其中打开文件列表用于存储进程打开的所有文件的属性,进程每打开一个文件就会使用一个结构体存储此文件属性,这个结构体称为文件描述符,所有的文件描述符放在一个数组中,此数组称为打开文件列表,进程可以打开的文件数量上限等于打开文件列表元素数量上限。
创建进程
一个进程可以创建另一个新进程执行,新进程称为创建者的子进程,拥有同一个父进程的子进程之间称为兄弟进程。
所有的用户进程最终都会关联到init进程,init进程的编号为1,它是linux启动后执行的第一个用户进程。
同源子进程
进程复制自身数据创建的子进程称为同源子进程,同源子进程的作用类似线程,早期的操作系统没有线程功能,当一个进程需要同时处理多组数据时,就将自身复制一份创建一个子进程,新进程与父进程同时处理数据,从而实现类似线程的功能,之后这种执行方式就被流传下来,Linux也继承了这种方式,Linux是没有线程的,它通过同源子进程模拟线程功能,或者理解为同源子进程就是线程。
同源子进程有两种创建方式,主要区别就是全局数据是否与父进程共用:
1.使用fork函数创建同源子进程,全局数据不共用,子进程会复制一份父进程的全局数据供自己使用,子进程修改全局数据不影响父进程。
2.使用clone函数创建同源子进程,可以设置哪些全局数据为共用,任何一个进程修改了共用的全局数据后,其它进程使用的也是修改后的值,这种同源子进程更类似线程功能。
最早的同源子进程会复制父进程中所有的数据,但是同源子进程使用的函数与父进程完全相同,只是全局变量可能会不同,这份相同的指令数据没必要复制一份,父进程与子进程共同使用即可,这样既节省内存、又节省了复制数据占用的CPU执行时间,因此操作系统对同源子进程的创建步骤进行了简化,创建同源子进程时只是创建一个进程描述符、并分配一个栈空间,不复制指令数据,两个进程使用同一份指令数据,而全局变量按需复制,当同源子进程或父进程任何一方需要修改全局变量时,两个进程将不再共用此全局数据,系统内核会将需要修改的全局变量复制一份给同源子进程,供其单独使用,这种管理方式称为写时复制,有了写时复制技术以后,同源子进程也称为轻量级进程。
异源子进程
进程执行新程序时创建的子进程称为异源子进程,异源子进程的数据与父进程完全不同。
用户进程中,非init进程创建的异源子进程会自动变成inti的子进程,由init负责管理,不再属于创建者的子进程,而内核进程创建的异源子进程依然属于创建者的子进程,进程关系不会发生改变。
创建异源子进程最直接的方式就是操作系统分配一段内存空间,然后将程序数据写入到此处,之后执行程序,但是linux却不是这样做的,linux会首先删除父进程在内存中的数据,之后将新程序的数据写入内存,从而创建一个新进程,为了防止父进程被删除,创建异源子进程时需要首先创建父进程的同源子进程,之后使用同源子进程创建异源子进程,至于为什么使用这种创建方式,完全是因为历史原因,而不是故意设计成这样,虽然看似这种创建方式不太合理,但是因为有写时复制技术也不会降低性能。
总结,子进程分为两类,同源子进程的作用是模拟线程,异源子进程是一个真正的独立进程,这两种进程名称只在本文中,其它文章将异源子进程称为子进程、同源子进程称为轻量级进程。
结束进程
进程结束执行分为主动与被动两种情况。
1.主动退出,进程内代码执行完毕,之后自行使用系统调用告知操作系统要求退出,操作系统接收到退出信息后,删除它占用的内存,不再调度此进程。
2.被动退出,进程自身没有执行完毕,但是被其它程序发送信号要求其退出,或者操作系统因为某些原因强制其退出。
无论进程使用哪种方式退出,默认情况下操作系统都会向其父进程发送一个信号,告知有子进程退出,之后父进程可以通过进程描述符查询是哪个子进程退出、以及退出原因,并清理僵尸子进程的描述符,父进程也可以设置不接收子进程退出事件,此时子进程结束运行后直接删除其在内存中的所有数据,包括进程终止后的描述符,直接进入死亡状态。
进程调度
操作系统将需要执行的进程放在一个队列中,称为进程队列,只有进程队列中的进程才会被操作系统调度执行,暂停执行的进程会在进程队列中移除,在多核心的处理器中,每个处理器内核都绑定一个进程队列,从而确定此内核要执行哪些进程。
队列中的进程并非执行完毕后再切换到下一个进程,操作系统将CPU的执行时间分为多个片段,操作系统以时间片为进程分配执行时间,时间片到期后进程暂停,切换到下一个进程执行,从而实现多个进程同时执行,时间片的长度并不是固定的一种,而是有多种,对于CPU消耗型的进程会分配长度更大的时间片,对于IO设备消耗型的进程会分配短的时间片,进程切换执行时,操作系统会将寄存器的值保存在内存中,进程恢复执行前首先还原寄存器的值,之后执行进程。
进程死锁
进程死锁最常见的情况是两个进程都需要同时独占两个文件的使用权,进程1占用了文件1,进程2占用了文件2,此时两个进程都在等待对方释放另一个文件的占用权,从而出现无限期等待。
操作系统无法避免进程之间的死锁,若增加管理进程死锁的功能则操作系统的工作会大量增加,从而导致程序执行效率降低,进程死锁问题只能靠进程自己解决,比如进程在等待资源时设置时间上限,若超时则释放已占用资源,让其它进程使用。
【文件管理】
存储器分区
辅存的存储空间可以分为多个区域使用,每个区域称为一个分区,每个分区的容量、开始地址、结束地址、其它相关属性使用分区表记录,分区表不属于任何分区,它占用存储器最开始的存储单元,辅存必须创建分区表才能使用,若无需分为多个区域使用,可以在分区表中只分配一个分区,占用整个存储器空间。
分区表按类型分为两种:MBR、GPT。
MBR 分区表
MBR是旧的分区表类型,与操作系统引导程序一块存储,占用辅存的第一个扇区或第一个页,总长度512字节,前446字节存储操作系统引导程序,后64字节存储分区表,最后两个字节为结束标记,值为0x55和0xaa。
64字节的分区表再分为四组,每组16字节,每组存储一个分区的信息,使用MBR分区表的辅存最多创建4个分区,并且只能使用最大2TB的存储器。
GPT 分区表
GPT是新型的分区表,它使用更多的扇区或页记录分区的属性信息,与MBR分区表相比优势如下:
1.可以创建更多数量的分区。
2.管理容量超过2TB的存储器。
3.使用更大容量的操作系统引导程序。
分区表类型与操作系统引导程序类型绑定,使用Legacy引导方式时,引导程序只能放在MBR分区表中,使用UEFI引导方式时,引导程序只能放在使用GPT分区表的FAT32(一种文件系统)分区中。
文件系统
实现或记录某种功能的一组二进制数据称为一个计算机文件,分区需要创建文件系统才能使用,文件系统的作用是记录文件占用了哪些存储单元,文件系统以逻辑块为单位进行管理,逻辑块的大小是扇区或页的倍数,比如512B、1KB、2KB、4KB,创建文件系统时可以自行选择所需的逻辑块大小,文件以逻辑块为单位占用存储单元,一个逻辑块只能存储一个文件的数据,若只使用了逻辑块的一部分,则剩余部分不能再存储其它文件。
文件系统为每个文件分配一个唯一的数字编号,通过文件编号调用此文件,因为编号不方便人记忆,所以每个文件还会分配一个文件名称,文件名与文件编号绑定,从而可以使用文件名调用文件。
文件系统中的文件可以继续分类管理,每一类文件的信息使用另一个专用文件存储,此文件称为目录文件,目录文件存储了归属此目录的文件名称和其它基础属性,分类可以嵌套使用,也就是可以在目录中创建子目录,使用目录对文件进行分类管理的优势是方便查询,操作一个文件时需要同时指定文件所属目录的名称以及文件的名称,这个完整的名称称为文件路径,文件系统会首先读取路径中的目录文件,并在其内部查询指定文件,之后操作文件。
文件分配表
文件分配表的作用类似分区表,它占用开头的一组逻辑块,用于记录文件属性信息、逻辑块的分配信息,剩余逻辑块用于存储文件内容,这些逻辑块也称为数据块。
文件属性使用一个结构体记录,英文名为index node,中文名为索引节点,简称i节点,所有的i节点使用一个数组存储,称为inode表,inode表的元素编号就是文件编号,inode表的长度是有限的,长度用完后即使分区还有空余存储空间也不能再存储新文件。
ext4文件系统的i节点原型如下:
struct ext4_inode
{
__le16 i_mode; //文件类型与基础访问权限
__le16 i_uid; //文件所属用户编号低16位
__le32 i_size_lo; //文件大小,单位字节
__le32 i_atime; //文件最近访问时间
__le32 i_ctime; //文件创建时间
__le32 i_mtime; //文件最近修改时间
__le32 i_dtime; //文件删除时间,也就是文件放入回收站的时间
__le16 i_gid; //文件用户组编号的低16位
__le16 i_links_count; //文件硬链接数量
__le32 i_blocks_lo; //文件占用块数量
__le32 i_flags; //文件打开状态标志
union
{
struct
{
__le32 l_i_version;
} linux1;
struct
{
__u32 h_i_translator;
} hurd1;
struct
{
__u32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT4_N_BLOCKS]; /* 文件内容占用逻辑块的编号 */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high;
__le32 i_obso_faddr; /* Obsoleted fragment address */
union
{
struct
{
__le16 l_i_blocks_high; /* were l_i_reserved1 */
__le16 l_i_file_acl_high;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__le16 l_i_checksum_lo; /* crc32c(uuid+inum+inode) LE */
__le16 l_i_reserved;
} linux2;
struct
{
__le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
__u16 h_i_mode_high;
__u16 h_i_uid_high;
__u16 h_i_gid_high;
__u32 h_i_author;
} hurd2;
struct
{
__le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
__le16 m_i_file_acl_high;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
__le16 i_extra_isize;
__le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
__le32 i_ctime_extra; /* extra Change time (nsec << 2 | epoch) */
__le32 i_mtime_extra; /* extra Modification time(nsec << 2 | epoch) */
__le32 i_atime_extra; /* extra Access time (nsec << 2 | epoch) */
__le32 i_crtime; /* File Creation time */
__le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
__le32 i_version_hi; /* high 32 bits for 64-bit version */
__le32 i_projid;/* Project ID */
};
成员i_mode记录文件的类型与基础访问权限。
文件系统将文件类型分为以下几种:普通文件、目录文件、设备文件(字符设备文件、块设备文件)、功能文件(FIFO文件、socket文件),其中设备文件与功能文件不占用数据块,设备文件绑定IO设备编号,读写设备文件会转换为读写IO设备端口,功能文件绑定内存地址,读写功能文件会转换为读写内存单元。
文件权限用于记录哪些用户拥有读、写、执行权限,之后会详细介绍。
成员i_block是一个数组,用于存储文件占用的数据块编号,i_block的长度为15,每个元素作用如下:
1.元素1-12,直接记录数据块的编号,若文件占用的数据块不超过12个,则使用前12个元素即可完全记录其占用的数据块,这12个元素可以看做是一级指针,直接指向数据块。
2.元素13,类似二级指针,指向一个数据块,此数据块中的数据存储文件占用的数据块编号。
3.元素14,类似三级指针,同上。
4.元素15,类似四级指针,同上。
当文件需要用到i_block的第15个元素时,文件将会占用( 12 + 二级指针容量 + 二级指针容量*三级指针容量 + 二级指针容量*三级指针容量*四级指针容量 )个数据块,超过此容量的文件不能存储,但是也很难有如此大的文件。
目录文件内容
文件名存储在所属目录中,目录本身也是个文件,内部包含一个数组,元素为结构体,每个元素存储一个文件的属性,此结构体有如下两种形式:
struct ext4_dir_entry
{
__le32 inode; //文件i节点编号
__le16 rec_len; //此结构体长度
__le16 name_len; //文件名长度
char name[EXT4_NAME_LEN]; //文件名
};
----------------------------
struct ext4_dir_entry_2
{
__le32 inode; //文件i节点编号
__le16 rec_len; //此结构体长度
__u8 name_len; //文件名长度
__u8 file_type; //文件类型,普通文件或目录文件
char name[EXT4_NAME_LEN]; //文件名
};
使用文件路径调用文件时,文件系统会首先寻找指定的目录文件,之后在目录文件中通过文件名查询到指定文件,从而找到此文件的i节点,之后调用文件,若文件直接存放在分区中,则此文件的属性使用一个特殊的目录文件记录,这个目录称为根目录,根目录用于记录直接在分区中存储的文件属性,文件系统通过文件路径查询文件时从根目录开始。
文件链接
连接,表示一个行为,链,表示连接所用的绳索,在计算机中经常有人将链称为链接,同时又有人将连接也称为链接,此文中链接表示名词,表示连接行为所用的数据,实际上链接简称为链更合适,以免与连接混用导致误解。
在文件系统中,文件路径也称为文件硬链接,硬链接与文件i节点绑定,一个文件只能有一个i节点,但是可以有多个硬链接,也就是可以为一个文件绑定多个名称、或者将一个文件名称同时分配到多个目录内,此时使用任何一个硬链接都会调用同一个文件,为了避免造成混乱,普通用户很少会为同一个文件创建多个硬链接,这种行为只在某些功能复杂的程序中使用。
对应的还有文件软链接,文件软链接是一个独立的文件,只不过此文件使用字符串存储了另一个文件的路径,读写软链接文件时会转换为读写其指向的文件,但是删除软链接文件不影响其指向的文件,只是删除软链接文件本身,而通过硬链接删除文件会直接删除其指向的文件。
使用硬链接和软链接都可以调用其指向的文件,区别在于文件硬链接是通过文件系统实现的,链直接存储在文件分配表中,文件软链接是通过操作系统实现的,链存储在另一个文件中,文件软链接建立在文件硬链接功能之上。
文件删除
删除文件时只是将文件i节点编号、文件所属目录条目编号记录为废弃状态,新文件可以重新使用这些废弃位置存储文件属性,并不会将文件属性、文件内容占用的存储单元全部设置为0,基于以上原理,删除的文件是可以恢复的,只需被删除文件的i节点、以及文件内容占用的存储单元没有被其他文件再次占用、保留了原来的状态。
如果需要将文件彻底删除,可以将文件占用存储单元的状态全部打乱,全部写入0、或1、或其它随机数字,文件粉碎机就是此原理,但是注意一点,粉碎的只是文件内容,不包含存储在目录文件中的文件名称,文件名称还是可能被恢复的,若需隐藏粉碎文件的名称,可以在粉碎文件之前修改文件名为随机字符。
使用操作系统提供的文件管理器删除文件时,并非真的删除文件i节点,而是将文件移动到一个备用目录中,这个目录称为回收站,回收站的作用是方便用户恢复误删除的文件,回收站会定期清理内部过期文件,文件移动到回收站后长期不恢复才会真正删除其i节点。
虚拟文件系统
Linux需要兼容多种文件系统,每一种文件系统都有不同的使用方式,这为程序操作文件带来麻烦,若程序为每一种文件系统编写一份操作文件代码将会非常繁琐,为此Linux研发了虚拟文件系统,虚拟文件系统提供了统一的操作文件方式,比如文件的创建、删除、修改、读取、写入操作,程序使用虚拟文件系统操作文件,之后操作系统判断分区使用的文件系统类型,再转换为此文件系统对应的操作方式。
目录使用规则
FHS
FSH是为操作系统制定的文件分类存储规则,文件按作用和类型分类存储在不同的目录,FHS规定使用一个分区作为主分区,也称为根分区,主分区内创建多个目录,分别存储不同分类的文件。
其它分区需要与根分区中的目录进行直接或间接的绑定才能使用,这个绑定所用目录称为挂载点,访问挂载点就等于访问其绑定的分区,挂载点不仅可以绑定分区,也可以绑定分区中的一个目录。
使用挂载点访问分区有如下两个优势:
1.不限制FHS规定的分类文件只能存储在根分区中,比如FHS规定根分区中的boot目录存储操作系统引导文件,若需要将这些文件单独存储在一个分区中,就可以将boot目录绑定另一个分区,而不是直接使用根分区中的boot目录存储。
2.扩展分区容量,在此分区中创建一个目录作为挂载点绑定另一个分区,即可增加此分区的容量。
安装Linux系统时需要设置使用哪个分区作为根分区,根分区使用 / 符号访问,根分区内的文件和目录使用 “/文件名” 的方式访问,比如 /boot,表示访问根分区中的boot目录。
FHS规定的根分区常用目录如下:
/boot,存放系统引导文件。
/etc,存放系统配置文件。
/run,绑定一个内存目录,Linux启动后会将各种系统相关信息使用内存文件存储,程序可以读取这些文件进行查询,因为绑定的是内存,所以每次操作系统启动后此目录都会清空。/srv,存放网络通信功能相关文件,比如HTTP、FTP、SMTP网络通信所用文件,个人用户通常会安装第三方浏览器、电子邮箱、和其它各种网络通信软件,这些软件会在自己的安装目录中存储数据,srv一般只在服务器中使用。
/dev,存放设备文件。/usr,存放操作系统提供给用户使用的一些文件,内部会定义很多子目录,文件会继续细分存储在usr的子目录中。
/usr/lib,存放链接库文件。
/usr/bin,存放可执行文件。
/usr/sbin,存放重要的可执行文件,一般只有root用户有权限执行。
/usr/include,存放各种C语言函数库文件。
/usr/share,存放文本文件,比如各种帮助文档。
/usr/local,存放用户程序,用户自己安装的软件可以放在这里。/var,存储操作系统运行、用户程序运行所需的经常变化的文件,比如系统日志、数据库、缓存文件,与usr类似,也是在内部创建各种子目录继续分类存储文件。
/home,存放普通用户主目录,每个用户都有一个专用的主目录,放在/home内,比如用户ali的主目录为/home/ali。
/root,root用户主目录。
/opt,存放用户自己安装的程序,与 /usr/local 相同。/tmp,存放临时文件,程序执行期间产生的临时文件放在这里,程序执行完毕既废弃,此目录内的文件应该定期清理,防止废弃文件占用过多存储空间。
/recovery,回收站目录,回收站内文件会在超过规定时间后自动删除。
/proc,绑定一个内存目录,linux启动后会自动创建一些内存文件存储进程相关信息。
/sys,绑定一个内存目录,存放一些说明系统内核和硬件信息相关的文件。/media,用于绑定外接媒体类存储器,比如光盘、软盘,需要在内部创建子目录作为挂载点,而不是直接绑定media。
/mnt,用于绑定临时添加的辅存,需要创建子目录作为挂载点。/lost+found,文件系统发生错误后,将丢失的一些文件碎片存放在这里。
文件路径指定方式
操作文件时需要指定文件路径,文件路径以 / 符号开头,/ 符号表示根分区,之后编写目录名、文件名,目录名与文件名之间也需要编写 / 符号,示例:/home/ali/a.txt,末尾的a.txt表示一个文件。
若需要指定一个目录,则需要在目录末尾添加 / 符号,示例:/home/ali/data/,data表示一个目录,目录末尾的 / 符号也可以省略。
相对路径
进程工作目录
当一个进程需要频繁操作一个目录内的文件时,每次都指定文件的完整路径很繁琐,为此linux为进程提供了工作目录的功能,工作目录表示本进程频繁使用的目录,指定工作目录内的文件时无需编写完整路径,只编写工作目录之后的路径即可,操作系统会自动将简写路径与工作目录进行组合,形成为完整路径。
比如一个进程设置/home/ali/为工作目录,若有一个文件的完整路径为/home/ali/data/a.txt,则在此进程中使用data/a.txt即可指定此文件,简写路径无需使用/符号开头。
环境变量
有些程序需要经常执行,但是这些程序放在不同的目录内,执行程序时需要输入程序的完整目录,每次都输入完整目录很麻烦,为此操作系统提供了环境变量功能,环境变量用于记录常用目录,使用这些目录中的文件时无需输入完整路径,使用简写路径即可,程序遇到简写路径后会在环境变量记录的目录中搜索简写路径指定的文件,并组合为完整路径,之后从文件系统中调用文件。
环境变量本身是一个字符串,存储在shell脚本文件中,环境变量有多个,分别服务于不同类型的简写路径,每个环境变量又可以存储多个路径,路径之间使用:符号隔开,其中PATH变量用于记录在终端执行程序时常用的目录,示例:PATH="/home/ali:/opt";,这里存储了两个目录。
存储环境变量的shell脚本文件如下:
1./etc/profile,此文件存储的环境变量对所有用户有效。
2./home/用户名/.bashrc,此文件存储的环境变量对指定用户生效。
进程使用环境变量时并非临时读取,而是在进程执行时就将其读取到内存中备用,所有的环境变量使用一个数组存储,这个数组称为环境列表,子进程执行时会复制一份父进程的环境列表,之后子进程可以按需修改自己的环境列表。
相对路径使用方式
有了工作目录和环境变量后,指定文件路径时就可以使用简写方式,这种简写的文件路径称为相对路径,完整路径称为绝对路径。
程序代码使用相对路径操作文件时,与自己的工作目录组合为完整路径,示例:fopen("a.txt", "a");。
预处理指令#include在<>符号内使用相对路径时,在环境变量记录的目录中查询指定文件,并组合为完整路径,示例:#include <stdio.h>。
预处理指令#include在""符号内使用相对路径时,与编译器工作目录组合为完整路径,示例:#include "ali.c"。
常用目录快速指定方式
1.使用.符号表示工作目录,比如工作目录为 /home/ali/,则.表示 /home/ali,注意.符号表示的目录末尾没有/符号,所以使用.符号调用工作目录内的文件时应该额外添加/符号,示例:./a.out。
2.使用..符号表示工作目录的上一层目录,比如工作目录为 /home/ali/,则..表示 /home 目录。
3.使用~符号表示本用户的主目录,比如本用户主目录为 /home/ali/,则~表示 /home/ali 目录。
注:程序进行读目录操作时,总会读取到.和..两个虚拟子目录,但是这两个子目录是不存在的,它们存在的作用只是方便快速访问,因为是虚拟的所以也不能删除,读取目录时直接忽略.和..即可。
Linux中的文件类型
Linux将文件按功能分为以下几种类型:
1.ELF文件,程序文件。
2.软链接文件,存储另一个文件的路径。
3.设备文件,绑定IO设备。
4.功能文件,存储进程间通信的数据。
5.用户文件,用户自建的各种类型文件统称为用户文件,比如:文本文件、图片文件、音频文件、视频文件。
其中设备文件、功能文件、用户文件都用于存储各种用户数据,用户程序需要频繁对其进行读写操作,它们的使用区别主要是读取文件行为的结果不同:
1.读取用户文件时,若没有数据可读则直接返回。
2.读取设备文件、功能文件时,若没有数据可读则进入暂停等待状态,直到有数据后恢复执行,此类文件也可以设置为不进入等待状态、直接返回。
设备文件
Linux为每个IO设备绑定一个设备文件,高级编程语言读写设备文件时会转换为读写IO设备端口,其中又分为字符设备文件、块设备文件,分别用于按字节读写数据的IO设备(键盘、鼠标)、按块读写数据的IO设备(辅存)。
设备文件存储在/dev目录中,常用设备文件命名方式如下:
1.sd,绑定SATA、USB接口连接的辅存设备,名称添加字母区分不同的辅存,比如sda、sdb、sdc,之后再添加数字区分不同的分区,比如sda1、sda2,分区编号不一定是连续的,可以在进行分区时自定义分区编号。
2.nvme,绑定使用NVMe协议的M.2接口连接的辅存设备。
3.cdrom,光盘驱动器。
4.tty,终端,添加数字区分不同的终端,比如tty1、tty2。
功能文件
有些进程或系统调用的执行需要与其它进程交换数据,这些数据使用专用类型的文件存储,因为没有人为这类文件起名,所以我将其称为功能文件,功能文件是在内存中创建的文件,某些功能文件还会绑定一个文件系统路径,通过此路径读写功能文件时会读写绑定的内存文件。
每个进程都会自动打开三个功能文件:标准输入文件、标准输出文件、标准错误文件,这三个文件是操作系统自动为进程创建的,用于存储终端输入输出数据,具体作用如下:
1.标准输入文件,文件描述符编号0,存储终端输入数据,程序读取数据后,读取的数据会在此文件中删除,未读取的数据依然保存在标准输入文件内。
2.标准输出文件,文件描述符编号1,存储终端输出数据,程序写入数据时,以增加的方式写入,不会覆盖原有数据,终端读取其中的数据进行显示,读取后的数据会被删除。
3.标准错误文件,文件描述符编号2,存储程序执行错误后操作系统在终端输出的数据,用于告知用户发生了什么错误,当然用户程序也可以向其中写入数据,程序被禁用终端输出功能后可以通过向标准错误文件写入数据实现输出功能。
printf函数实现终端输出的原理就是向标准输出文件写入数据,程序可以自行向标准输出文件写入数据实现输出功能。
#include <unistd.h>
int main()
{
write(1, "阿狸\n", 7); //输出“阿狸”并换行
return 0;
}
Linux如何识别文件类型
在Windows系统中,文件的类型由文件名中的后缀确定,文件名使用.符号分为两部分,.符号之后为后缀名,比如ali.exe,后缀exe表示可执行文件,后缀名默认不显示。
在Linux系统中,系统内核操作文件的类型由文件头决定,文件内部的起始位置会存储一些说明文件类型、文件属性的数据,这些数据称为文件头,使用文件头确定文件类型的方式对程序来说很方便,但是对计算机用户却不方便,用户无法通过文件名快速确认文件类型,为此很多人也会参考Windows系统的方式,使用后缀名说明文件的类型,比如后缀out表示可执行文件、后缀o表示目标文件、后缀so表示动态链接库文件,但是这些后缀只用于计算机用户自行使用,Linux会忽略它、依然使用文件头确定文件类型,比如你将一个可执行文件命名为a.so依然可以直接执行,而在Windows中这种类似行为会出错、无法执行。
在Linux系统中,用户文件的类型使用后缀名确定,文件后缀名有跨平台的通用规则,比如文本文件后缀txt,不同的操作系统、不同的文件查看器都使用此规则确定用户文件类型,若将一个音频文件命名为a.txt则不会被音频播放器识别为音频文件,但是可以被文本文件编辑器打开,此时会将音频数据当做字符显示。有些功能简单的用户文件没有文件头,比如文本文件,它不需要使用文件头说明文件属性,它直接从文件起始处存储字符编码。
文件访问权限
此节内容需要首先了解系统用户相关知识,新手可以跳过本节首先学习用户管理,之后返回学习本节。
操作系统可以被多个用户使用,每个文件都有所属用户,文件权限用于说明文件在不同用户中拥有哪些使用权限,文件权限对目录也会生效,目录本身也是一个文件。
文件权限由文件系统记录,如果将文件复制到另一种类型的文件系统分区中,文件权限可能会丢失,不同文件系统对文件权限的记录方式不同,彼此不一定兼容,比如将文件从Linux使用的EXT4文件系统复制到Windows使用的NTFS文件系统中,记录的文件权限会消失。
文件系统记录文件权限时,会将用户分为如下三类:文件所属用户(文件属主)、文件所属用户所在用户组的用户(文件属组)、其它用户,文件系统可以为这三类用户分别设置不同的权限,但是文件基础权限对root用户不生效。
另外Linux还支持使用ACL(访问控制列表)方式设置权限,使用ACL可以单独为每个用户设置不同的权限,而不是只将用户分为三类设置不同的权限,但是并非所有的文件系统都支持ACL功能。
基础权限
1.读权限,表示可以读取文件内容,目录读权限表示可以查询目录中的文件和子目录。
2.写权限,表示可以向文件写入数据、删除数据、修改数据,目录写权限表示可以在目录中新建文件、删除文件、修改文件名。
3.执行权限,普通文件的执行权限只针对可执行文件,目录文件的执行权限表示用户可以将此目录设置为工作目录。
以上权限产生的限制对root用户不生效。
s权限
文件权限除了以上三个基础权限外,还有很多特殊权限,常用的特殊权限是s、t、i、a权限。
s权限只用于可执行文件,用户执行一个程序时,默认使用本用户的权限去执行,若设置了s权限,则任何有权限执行此程序的用户执行程序后,此程序都以文件属主的权限执行,而非执行者用户的权限去执行。比如root用户创建了一个程序,并设置其它用户拥有执行权限,其它用户执行此程序时默认以本用户的权限执行,若root用户对此程序设置了所有用户都拥有s权限,则其它用户执行此程序时会以root用户的权限执行。
s权限建立在执行权限之上,文件需要首先拥有执行权限才能添加s权限。
t权限
t权限只用于目录,若一个目录需要与其它用户共同使用,则创建者通常设置为所有用户都拥有写、执行权限,读权限只有自己拥有,防止其他用户删除目录内文件,但是其它用户依然可以通过猜测文件名的方式删除此目录中的文件,为了防止这种情况,可以对此目录设置t权限,此时其它用户只能在此目录中删除自己创建的文件,不能删除其它用户创建的文件。因为其它用户没有目录读权限,所以其它用户自己在此目录创建的文件也无法查询,需要由用户自己记录所有的创建文件名称。
此限制对root用户不生效。
i权限
i权限表示用户可以查看文件内容,不能删除文件,不能修改文件,此权限需要由root用户设置,一般用于在拥有写权限的目录中创建一个不可删除、不可修改的文件,对root用户也生效,文件添加了i权限后root用户也不可删除、不可修改,需要首先取消i权限。
a权限
a权限表示用户可以查看和增加文件内容,不能删除文件内容,不能删除文件本身,此权限需要由root用户设置,对root用户也生效,测试a权限功能时不能使用操作系统提供的文本编辑器向文件添加数据,可以自行编写代码使用写文件函数添加数据。
【ELF文件格式】
ELF文件是在linux中执行的程序文件,又分为以下四类:
1.可执行文件,可以自己在操作系统中执行的程序。
2.目标文件,不能自己执行的程序,需要被其它程序调用执行,目标文件会与调用者组合为一体。
3.动态链接库文件,链接库文件用于将目标文件打包存储,又分为静态链接库与动态链接库,动态链接库在调用者执行期间读取到内存中连接,而不是在编译期间连接,静态链接库不归类于ELF文件,静态链接库只是将多个目标文件打包,类似zip文件,而动态链接库要复杂的多。
4.核心转储文件,存储执行出错的进程,进程执行出错后操作系统将其终止执行,同时将内存中的进程数据保存一份到辅存中,用户可以读取核心转储文件查询进程终止前的状态与原因。
ELF文件内的数据可以分为四类,分别如下:
1.文件头,文件最开头的数据,用于说明文件类型、文件属性。
2.节,英文名为section,也有人称为段,程序的指令数据、数学数据、其它与运行相关的数据会按作用分为多组,每一组使用一个节存储。
3.节属性表,是一个元素为结构体的数组,每个元素存储一个节的属性信息。
4.程序节属性表,同节属性表,但是只用于存储指令数据节、指令执行相关节的属性。
以上四组数据在ELF文件中的存储顺序为:1文件头、2程序节属性表、3节、4节属性表。
文件头
文件头使用一个结构体定义,以64位ELF文件为例,文件头原型如下:
typedef struct
{
unsigned char e_ident[16]; //前4个字节数据分别为:7f 45 4c 46,第一个字节为删除键编码、后三个字节为ELF字母的编码,表示这是ELF文件。
//第5个字节说明ELF文件使用32位处理器指令还是64位处理器指令,值为1表示32位,值为2表示64位。
//第6个字节说明数学数据使用的字节序,值为1表示小端序,值为2表示大端序。
//第7个字节说明ELF文件的版本,目前只能设置为1。
//第8个字节指定程序使用的ABI的类型,通常设置为0
//第9个字节指定程序使用的ABI的版本,通常设置为0
//剩余字节保留不用,全部赋值为0
u16 e_type; //指定ELF文件的具体类型,值为1-4,1表示目标文件,2表示可执行文件,3表示动态链接库,4表示核心转储文件
u16 e_machine; //指定程序使用的CPU类型,值为62表示64位X86处理器,值为40表示32位ARM处理器,值为183表示64位ARM处理器
u32 e_version; //指定ELF文件的版本,目前只有一个版本,值固定为1
u64 e_entry; //指定程序的执行入口虚拟地址,虚拟地址从0x400000开始算起,若为目标文件、动态链接库,则此值为0
u64 e_phoff; //指定程序节属性表的文件内部地址,文件内部地址从0开始分配,文件头占用最开始的地址,程序节属性表在之后,若e_phoff为0表示不包含程序节属性表
u64 e_shoff; //指定节属性表的文件内部地址,若为0表示不包含节属性表
u32 e_flags; //程序运行在特殊处理器中时使用此数据设置某些信息,运行在x86处理器中时此数据设置为0
u16 e_ehsize; //文件头的长度,以字节为单位
u16 e_phentsize; //程序节属性表的元素长度,以字节为单位
u16 e_phnum; //程序节属性表的元素数量
u16 e_shentsize; //节属性表的元素长度,以字节为单位
u16 e_shnum; //节属性表的元素数量
u16 e_shstrndx; //指定节属性表中哪个元素存储shstrtab节信息,值为元素下标
} Elf64_Ehdr;
e_ident的第8-9个字节设置ABI信息,ABI全称为Application Binary Interface,中文名为应用程序二进制接口,它是一组规则的名称,是操作系统为程序制定的运行规则,比如:函数调用时参数如何传递、函数返回值如何存储、系统调用使用规则、异常功能使用规则,CPU在设计时会为了兼容ABI的某些规则进行优化,提升程序运行速度。
一个 x86-64 ELF 可执行文件的文件头:
7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 //e_ident
02 00 3E 00 01 00 00 00 //e_type - e_version
40 10 40 00 00 00 00 00 //e_entry
40 00 00 00 00 00 00 00 //e_phoff
18 39 00 00 00 00 00 00 //e_shoff
00 00 00 00 40 00 38 00 0B 00 40 00 1D 00 1C 00 //e_flags - e_shstrndx
上述数据使用16进制,按文件内部地址对字节逐个排序,x86处理器对多字节数据使用小端排序,查看多字节数据时需要将字节位置重新排序。
程序节属性表
程序节属性表紧邻文件头,是一个数组,元素为结构体,原型如下:
typedef struct
{
u32 p_type; //指定节的类型
u32 p_flags; //设置节的访问权限,最低位设置执行权限、第2位设置写权限、第3位设置读权限,对应位设置为1表示有此权限,指令节设置为可执行,全局变量节设置为可读可写,全局常量节设置为只读
u64 p_offset; //节的文件内部地址
u64 p_vaddr; //节的虚拟地址
u64 p_paddr; //节的内存物理地址,某些特殊操作系统需要使用,在linux中不使用此数据
u64 p_filesz; //ELF文件在辅存中存储时,此节的长度
u64 p_memsz; //ELF文件读取到内存中时,此节的长度,有些节只在程序执行时才会存储数据,比如.bss节,这种节只在内存中分配存储空间
u64 p_align; //节的内存地址对齐值,若值为0或1等同于无对齐要求
}Elf64_Phdr;
一个 x86-64 ELF 可执行文件的程序节属性表的第一个元素:
06 00 00 00 04 00 00 00 //p_type、p_flags
40 00 00 00 00 00 00 00 //p_offset
40 00 40 00 00 00 00 00 //p_vaddr
40 00 40 00 00 00 00 00 //p_paddr
68 02 00 00 00 00 00 00 //p_filesz
68 02 00 00 00 00 00 00 //p_memsz
08 00 00 00 00 00 00 00 //p_align
节
ELF文件中的节有很多种,这里只介绍可执行文件的基础节,动态链接库相关节在之后的文章中讲解。
------------------- 指令数据节 -------------------
.init,存储程序执行入口指令,用于程序执行前的基础初始化工作,代码由编译器自动生成,之后跳转到.text节执行,存储指令数据的节会被操作系统设置为只读。
.plt,调用动态链接库相关,代码由编译器生成。
.text,存储程序主体功能代码,程序执行前复杂的初始化工作代码在这里(编译器生成),用户自己编写的代码也存储在这里。
.fini,存储程序终止时执行的相关代码,代码由编译器生成。------------------- 数学数据节 -------------------
.rodata,存储全局常量,此节分配的内存页会被操作系统设置为只读。
.data,全局变量节1,存储定义时已赋值的全局变量。
.bss,全局变量节2,存储定义时未赋值的全局变量,编译后的文件此节不占用存储空间,程序在内存中执行时才会为此节分配存储空间,此节占用的内存单元会全部设置为0,定义全局变量不赋值的话初始值为0。
.comment,存储编译器版本信息,内部是一个字符串。------------------- 调试相关节 -------------------
.debug,存储调试程序时需要的相关信息,供调试器使用,比如数据类型、.text节中指令数据对应的高级语言源代码行号,使用gcc编译程序时添加 -g 参数会生成此节。------------------- 数据名相关 -------------------
.symtab,配合.strtab使用,内部包含一个Elf64_Sfm结构体数组,记录每个数据名在.strtab节的哪个位置。.strtab,存储代码中全局数据、全局函数的名称,对程序反汇编时看见的数据名由此节记录,使用gcc编译程序时可以添加-s参数不保留数据名。对于C语言程序,此节记录的是数据名原型,对于C++程序,此节记录的并非原始数据名,因为C++支持函数重载、符号重载、虚拟类型、命名空间,导致多个数据可以同名,编译器会在原始数据名的前后分别添加随机字符从而区分同名数据,另外C++的类成员名还会包含类名。
.shstrtab,存储其它节的名称,为节设置名称是编译器的工作,使用gcc添加-s参数将不会生成节名称。
节属性表
节属性表内部元素原型如下:
typedef struct
{
u32 sh_name; //本节的名称在shstrtab节中的位置,若为0表示此节没有名称
u32 sh_type; //节的类型
u64 sh_flags; //节的读写属性
u64 sh_addr; //节的虚拟地址,若为0则此节不会被读取到内存中
u64 sh_offset; //节的文件内部地址
u64 sh_size; //节的长度,单位为字节
u32 sh_link; //节属性表中另一个元素的下标,连接器有时需要知道两个节的联系,若不需要与另一个节有关联则设置为0
u32 sh_info; //附加信息
u64 sh_addralign; //节的内存地址对齐值,若值为0或1则表示无对齐要求
u64 sh_entsize; //有些节的内容是一个数组,此成员存储数组元素的长度,若节的内容不是数组则设置为0
} Elf64_Shdr;
一个 x86-64 ELF 可执行文件的节属性表的末尾元素(.shstrtab):
11 00 00 00 03 00 00 00 //sh_name - sh_type,其中sh_type的值为3,表示存储数据名的节(.strtab .shstrtab .dynstr)
00 00 00 00 00 00 00 00 //sh_flags
00 00 00 00 00 00 00 00 //sh_addr,存储数据名的节不需要读取到内存
2d 38 00 00 00 00 00 00 //sh_offset
03 01 00 00 00 00 00 00 //sh_size
00 00 00 00 00 00 00 00 //sh_link - sh_info
01 00 00 00 00 00 00 00 //sh_addralign
00 00 00 00 00 00 00 00 //sh_entsize
【用户管理】
同一台计算机经常需要被多人同时使用,此时就需要隔离多个用户,让每个用户都有自己的使用空间,每个用户对操作系统运行方式的设置都是独立的,互不影响,操作系统提供的多用户功能用于实现此功能,其中有一个固定的root用户,此用户没有使用限制,用于自由的使用计算机、并且管理其它用户。
操作系统的多用户功能对家庭用户来说不太常用,甚至很多普通计算机用户不知道操作系统可以设置多个用户,但是对于服务器用户来说多用户功能是必备的,服务器操作系统经常需要被多个用户同时使用、并且需要对不同用户设置不同的使用权限。
用户
用户名、用户编号
操作系统为每个用户分配一个数字编号,英文简称UID,操作系统使用此编号管理用户,因为数字编号不方便记忆,所以每个用户可以再设置一个名称,用户名与用户编号绑定。
用户主目录
每个用户都会分配一个自己专用的目录,称为用户主目录,其他用户不能访问本用户的主目录(root用户除外)。
用户权限
用户有权限级别之分,低级别用户只能访问属于自己的数据,不能访问其它用户的数据,也不能设置操作系统的重要功能,高级别用户可以访问低级别用户的主目录,可以使用低级别用户的文件,可以设置操作系统的重要功能。
用户的登录、退出
操作系统启动后,需要用户使用用户名、密码进行身份认证,认证通过后被操作系统允许使用计算机,这个行为过程称为登录。
用户不再使用计算机,退出操作系统的身份认证,称为退出、或者登出。
用户类型
安装Linux操作系统时会自动创建一个名为root的用户,它是权限最高的用户,用户编号为0,root对操作系统的使用不受任何限制。
编号1-999的用户称为系统服务用户,每个daemon都使用一个独立的用户去运行,从而防止daemon因自身漏洞导致被人恶意利用,此类用户无需登录。
编号1000及以上的用户为普通用户。
用户组
操作系统用户可以分组管理,方便为多个用户同时设置相同的权利,用户组也有自己的名称和数字编号(GID)。
用户必须加入用户组才能使用,并且一个用户可以同时加入多个用户组,若创建用户时不指定其加入的用户组,则操作系统会自动创建一个用户组并加入,新建用户组的名称与用户的名称相同。
用户属性文件
存储用户属性的文件有如下两个:
/etc/passwd,存储用户密码之外的所有属性信息,包括用户名、用户编号、所属用户组、用户组编号、用户主目录、用户登录的终端、其它信息,这个文件可以被所有用户访问。
/etc/shadow,存储用户密码,密码经过加密,并非使用明文存储,此文件只能被高级别用户访问,普通用户没有访问权利。
用户组属性文件
存储用户组属性的文件为/etc/group,内部存储了用户组名、用户组编号、用户组密码、包含的用户、其它一些相关信息。
【终端】
以前的计算机有控制台(console)、终端(terminal)两种输入输出设备。
控制台是一种简单的IO设备,用于提供基础功能的控制开关、告知用户工作状态,这很像现在的自动洗衣机控制面板,用户通过按键发出数据控制洗衣机运行所需功能,洗衣机通过点亮不同的指示灯告知用户工作状态。
终端是一种复杂的IO设备,用于向CPU输入字符编码、接收CPU发出的数据,早期的终端使用穿孔纸带存储数据并向CPU输入,使用打印机在纸张上打印字符输出数据,之后的终端开始使用键盘和显示器进行输入输出数据。
计算机有多用户同时使用的需求,每个用户都需要对计算机进行输入输出数据操作,此时每个计算机用户都需要对计算机连接一终端,一台计算机可以连接多个终端,每个终端供一个用户使用,用户可以在这里进行登录,之后开始使用计算机,多个终端也可以同时登录同一个用户,若一个用户需要同时执行多个程序、并且每个程序都需要使用终端进行数据输入输出,就可以在多个终端登录同一个用户,之后在多个终端分别执行程序。
现在的计算机直接集成一套完整的基础IO设备,已经没有之前的控制台和终端,但是某些计算机(服务器、工作站)依然有多用户同时使用的需求,依然需要多个用户同时进行数据的输入输出,为此linux使用一个程序模拟硬件终端,linux启动后默认执行7个终端进程,模拟7个硬件终端设备,用户可以使用 ALT +(F1 到 F7) 组合键切换不同的终端使用,其它用户自己携带一台计算机通过网络连接服务器进行多用户同时使用,用户自己携带的计算机相当于服务器的终端。
虽然现在的终端已经不再是硬件IO设备,而是变成了一个进程,但是linux依然使用设备文件的方式管理终端,而不是使用功能文件,终端设备文件的名称以tty开头,据传这是teletype的简写。
执行控制台程序
使用终端进行数据输入输出的程序称为控制台程序,执行控制台程序时需要在终端输入此程序的路径,操作系统根据此路径寻找程序并执行,比如:/home/ltfw/a.out,通过终端执行程序后,进程自动使用此终端进行数据输入输出,进程使用的终端被关闭后,进程也会终止执行。
在终端内输入程序路径时可以使用相对路径,相对路径与环境变量中记录的路径组合为完整路径,若需使用终端工作目录与相对路径进行组合,需要在路径中使用.符号表示工作目录,示例:./a.out。
控制台程序参数
执行控制台程序时,可以同时向程序传输一些参数,这些参数会作为程序main函数的参数,参数可以有多个,多个参数之间使用空格分开,或者将不同的参数放在""符号内,在终端内输入的程序路径和程序参数统称为命令。
./a.out a 一个参数
./a.out "a" 一个参数
./a.out a b 两个参数
./a.out a "b c" 两个参数,引号内的字符算一个参数
C语言规定程序的main函数可以设置两个参数,参数的类型和名称是固定的,原型如下:
int main(int argc, char* argv[])
参数argc,存储用户在终端输入的字符串个数,用户输入的数据以空格或""符号分割为多个字符串,注意输入的程序路径也算入计数,所以argc的值最小为1。
参数argv,用户在终端输入的所有字符串放在一个数组中,相当于一个二维数组,argv指向此二维数组,二维数组的第一个字符串是输入的程序路径,剩余字符串是程序参数,末尾字符串为0。
argv参数还可以定义为如下形式:char ** argv,数组在传参时会转换为指针,二维数组转换为双层指针。
#include <stdio.h>
int main(int argc, char* argv[])
{
for(int i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
多数编译器还支持main添加第三个参数,如下:
int main(int argc, const char **argv, const char **envp)
参数envp指向本进程所用的环境列表,环境列表的有效元素之后也是存储一个0。
#include <stdio.h>
int main(int argc, const char **argv, const char **envp)
{
for(int i = 0; envp[i]; i++) //执行条件 envp[i] 功能等于 envp[i] != 0
{
printf("%s\n", envp[i]);
}
return 0;
}
前台进程与后台进程
多个进程都通过一个终端运行时,每个进程都使用此终端,但是一个终端只能为一个进程提供数据输入输出服务,使用终端输入输出服务的进程称为前台进程,不使用终端输入输出服务的进程称为后台进程,后台进程运行时不使用终端,但是在运行结束后会将之前输出的所有数据一次性输出到终端。
执行程序时可以设置程序以后台进程方式运行,示例:./a.out &,在命令之后添加空格和 & 符号表示进程以后台方式运行,注意&符号并不是传递给执行程序的参数。
shell
shell也称为外壳程序,用于解释在终端输入的命令,命令都是通过shell解释之后送往终端的,shell不是具体程序的名称,凡是提供解释命令功能的程序都称为shell,linux提供了两种shell,分别是 /bin/sh 和 /bin/bash。
shell可以在命令中添加一些符号完成特殊功能,常用符号如下:
------------ 通配符 ------------
*符号,表示任意类型、任意数量(包括0个)的字符,比如*.c表示任意以.c结尾的字符串,包括名称只有.c的字符串,比如a*.c表示任意以a开头、并以.c结尾的字符串,比如*ali*表示任意包含ali单词的字符串。
?符号,表示任意类型的一个字符,比如a?b表示以a开头、中间是任意单个字符、末尾是b的字符串,比如?b表示任意一个字符开始、并以b结尾的字符串。
[]符号,表示[]符号内指定的一个字符,不能单独使用,需要与其他字符组合使用,比如a[1,2,3]b,表示a1b、a2b、a3b这些字符串,也可以指定字母或数字的范围,比如a[a-z]b,表示中间的字母可以是从a-z。
------------ 多命令执行 ------------
;符号,执行多个命令时,使用;符号分割不同的命令。
&&符号,同上,分割多个命令,之前的命令解释成功时才会执行之后的命令。
||符号,同上,分割多个命令,之前的命令解释失败时才会执行之后的命令。
------------ 输入输出重定向 ------------
>符号,将程序在终端输出的内容存储到指定文件内,以覆盖的方式写入文件。
>>符号,同上,以增加数据的方式写入文件。
<符号,读取一个文件中的数据作为输入数据。
通配符
通配符用于查询符合要求的字符串,shell会将查询到的字符串添加到命令中,下面制作一个删除文件的程序,并编译为 ~/a.out。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
if(argc > 1)
{
for(int i = 1; i < argc; i++)
{
unlink(argv[i]);
}
}
return 0;
}
此程序使用参数接收要删除文件的路径,若需要删除一个目录中指定类型的文件,无需用户手动查询,也无需程序自己查询,使用通配符让shell进行查询即可,示例:~/a.out ~/text/[*.txt],执行~/a.out程序,并由shell查询~/text/目录中所有以.txt结尾的文件,之后将符合条件的文件路径输入到~/a.out的参数。
多命令执行
通过终端执行程序时,可以同时执行多个程序,使用; && ||符号连接多个命令即可,此时第一个命令作为前台进程,剩余命令作为后台进程。
./a.out && ./b.out
终端输入输出重定向
输出重定向用于将进程向终端输出的数据存储在指定文件内,若文件不存在则自动创建文件并写入数据。
./a.out > /home/ali/a.txt,将./a.out输出的数据存储在指定文件内,若文件中有内容则清空内容再写入。
./a.out >> /home/ali/a.txt,以增加数据的方式写文件,不覆盖文件原有内容。
输入重定向用于读取一个文件中的数据写入到进程使用的标准输入文件内。
./b.out < /home/ali/b.txt
【桌面系统】
图形界面程序
程序执行时需要与用户交流数据,控制台程序使用终端进行数据输入输出,用户在终端输入字符控制程序执行自己所需的功能,功能执行完毕后在终端显示字符输出执行结果,这种方式使用不方便,为了更方便的让程序与用户交流数据从而有了图形界面程序。
图形界面程序会在显示器中显示至少一个独立的图形,这个图形称为程序界面(英文简称GUI),程序界面内部可以显示字符、图片、动画,从而以更灵活的方式输出数据,程序界面内部可以继续嵌套其它图形,嵌套的图形称为控件,控件还可以继续嵌套子控件,控件与程序功能进行绑定,一个功能可以绑定多个控件,此功能使用绑定的控件进行数据输入输出,硬件输入设备增加了鼠标,鼠标按键发出的数据会传输给控件,从而执行与控件绑定的功能,功能执行完毕后将执行结果输出到控件中,图形界面内会显示一个可以移动的指针,移动鼠标会控制指针移动,指针在哪个控件之上鼠标按键发出的数据就传输给哪个控件。
图形界面程序不区分前台进程与后台进程,每个程序界面都可以直接输出数据,不会有控制台程序争抢终端使用权的情况,但是接收数据的图形界面只能有一个,鼠标发出的数据通过指针位于哪个界面之上确定要发送给哪个界面,而键盘发出的数据需要操作系统设置哪个界面进行接收,这个接收数据的界面称为活动界面,数据是发送给界面内的控件的,所以界面内的控件也区分活动控件,活动界面可以通过鼠标移动指针到指定界面后按左键选择,也可以使用键盘中的 alt + tab 组合键在所有的程序界面中选择。
X Window 与 Wayland
做大型软件项目之前需要首先进行规划,分析此软件都需要实现哪些子功能、每一种功能需要如何实现、不同功能之间如何交流数据,之后根据分析结果做出制作此软件的方式以及代码编写规则,X Window就是制作图形界面的规划结果,X Window是一组规则的名称,而不是指一个具体的程序或文件,程序图形界面的运行不仅需要程序自身实现相关功能,还需要操作系统提供功能支持。
现在使用的X Window是第11个版本,简称为X11,X11的正式版发布于1988年,比linux内核还要早,可以说是早已过时,Wayland正是用于代替X11出现的。
桌面子系统
桌面子系统是操作系统的一个组件,用于以图形界面程序的方式提供用户使用计算机时所需的基础功能,比如:文件管理、文件编辑、运行程序、系统功能设置。
用户登录后进入的界面称为桌面,桌面的显示区域占用整个屏幕,其中大部分的区域被一个容器控件占用,这个控件与用户主目录绑定,并以子控件的方式显示其中的文件。
在Linux中桌面系统不与系统内核绑定,不属于内核的一部分,而是在内核中运行的用户程序,桌面系统绑定系统内核的第一个终端,桌面系统因执行错误终止运行后,操作系统依然可以切换到其它终端继续正常使用(在deepin中使用 ctrl + alt + f1至f7 切换内核提供的7个终端),而Windows的桌面系统是系统内核的一部分,桌面系统因故障终止运行后,操作系统也会随之终止。
常见的Linux桌面系统有:
GNU项目的GNOME,基于GTK开发。
开源社区的KDE,基于QT开发。
deepin的DDE,基于QT开发。
【系统服务】
计算机程序的作用是为人服务,操作系统提供了一些基础的服务功能,称为系统服务(system service),比如:打印文件、网络防火墙、图形界面支持功能,服务是通过程序实现的,实现系统服务的程序称为daemon,很多人将daemon直接称为系统服务,daemon是组成操作系统的一个模块,但不属于操作系统内核,daemon使用用户程序指令权限运行。
daemon与普通程序的区别在于运行方式,daemon需要与操作系统一同启动,系统内核启动后会自动调用daemon运行,即使没有用户登录也会运行,用户可以自己设计一种系统服务,并制作一个daemon实现此服务的功能,之后将daemon设置为自动启动,具体方式是在/etc/rc[0-6].d/目录中创建shell脚本文件并编写脚本代码实现,这里不介绍脚本文件的编写方式。
【内核运行模式】
linux的运行方式又分为7个种,使用编号0-6表示,每一种称为一个模式,也有人称为7个运行级别,不同的运行模式会由init进程启动不同的功能,具体启动什么功能由 /etc/rc[0-6].d/ 这6个目录内的脚本文件确定,可以使用runlevel命令查看现在使用的运行模式,使用init命令更改运行模式(需root用户权限),参数为模式编号,示例:init 3。
编号0模式
启动关机功能,进入此模式后会执行正常关闭计算机操作。
编号1模式
单root用户模式,禁用网络远程登录功能,只能在本地进行操作。
编号2模式
多用户终端模式1,不启动NFS服务(通过网络管理另一台计算机文件)。
编号3模式
多用户终端模式2,启动NFS服务。
编号4模式
保留未用。
编号5模式
多用户图形界面模式,启动图形界面支持服务。
编号6模式
启动重启功能,进入此模式后会执行重启计算机操作,正常关闭服务以及用户程序。
标签:文件,00,操作系统,17,使用,用户,内核,Linux,进程 From: https://blog.csdn.net/m0_61996690/article/details/137523851