深信服面经
字符复制函数:
https://www.cnblogs.com/lxy-xf/p/11517873.html
问题主要有两个:
1.缓冲区溢出:如果src的长度大于dst的长度,则将赋值到dst的\0外,造成缓冲区溢出。
2.内存重叠:如果dst位于src和src+strlen(src)之间,则如果从头开始复制会造成内存重叠,如果以\0为终止的方式复制则永远不会停止下来
strcpy,strncpy,strlcpy:
strcpy:
传入 dst和src,以\0为终止,可能造成缓冲区溢出
strncpy:
复制n个字符到dst区域,结尾不加\0.
strlcpy:
strcpy的安全版本,传入参数为dst,src和sizeof(dst)。如果src大于dst的长度,则会截断为sizeof(dst)大小而防止缓冲区溢出。
内存对齐:
https://www.douban.com/group/topic/128339995/
1.数据成员的对齐:放置在min(数据成员长度,默认对齐长度)的整数倍位置
2.结构体整体对齐:补齐min(最大数据成员长度,默认对齐长度)的整数倍位置
string的实现
https://www.cnblogs.com/zhizhan/p/4876093.html
如何向其他进程发信号
kill函数
UDP使用connect,bind:
UDP是一个无连接的协议,因此socket函数connect似乎对UDP是没有意义的, 然而事实不是这样。 一个插口有几个属性,其中包括协议,本地地址/端口,目的地址/端口。 对于UDP来说,socket函数建立一个插口;bind函数指明了本地地址/端口 (包括ADDR_ANY,通配所有本地网络接口);connect可以用来指明目的地 址/端口; 一般来说,UDP客户端在建立了插口后会直接用sendto函数发送数据,需要 在sendto函数的参数里指明目的地址/端口。如果一个UDP客户端在建立了插 口后首先用connect函数指明了目的地址/端口,然后也可以用send函数发送 数据,因为此时send函数已经知道对方地址/端口,用getsockname也可以得 到这个信息。 UDP客户端在建立了插口后会直接用sendto函数发送数据,还隐含了一个操作, 那就是在发送数据之前,UDP会首先为该插口选择一个独立的UDP端口(在1024 -5000之间),将该插口置为已绑定状态。如果一个UDP客户端在建立了插口后 首先用bind函数指明了本地地址/端口,也是可以的,这样可以强迫UDP使用指 定的端口发送数据。(事实上,UDP无所谓服务器和客户端,这里的界限已经模 糊了。) UDP服务器也可以使用connect,如上面所述,connect可以用来指明目的地址 /端口;这将导致服务器只接受特定一个主机的请求。
socket相关函数
TCP编程:并发服务器
服务器端:
socket( int af, int type, int protocol);
af为ip协议种类(ipv4/ipv6),type为协议(数据流/用户数据报)
int bind(int sockfd, const struct sockaddr, socklen_t addrlen);
将主动套接字sockfd绑定到对应的端口,这样监听对应的端口
sockfd为监听套接字,sockaddr为绑定的端口和IP,对服务器来说IP为INADDR_ANY,即任意IP;端口为要监听的端口。addrlen为sockaddr的长度
注意任何对sockaddr的写入都要转为网络字节序,对端口使用htons,对IP使用htonl.
int listen(int sockfd, int backlog);
listen将主动套接字转换为被动套接字,成功返回0,否则返回-1
sockfd为主动套接字,backlog为队列长度
内核为listendfd维护两个队列:
一个为未完成连接队列,用于保存那些正在完成三路握手的套接字,也就是说此队列的套接字此时正处于SYN_RCVD状态
一个为已完成连接队列,用于保存那些已经完成三路握手的套接字
两个队列长度和不能超过backlog
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd为监听套接字,addr为客户端的信息,addrlen为addr长度
返回一个已连接套接字
注意addr中存放的信息此时为网络字节序,要是用ntohl/ntohs来转换
客户端:
socket:同上
int connect(SOCKET s, const struct sockaddr * name, int namelen);
name填对端的信息,namelen为name长度
connect后发起三次握手
UDP编程:迭代服务器
服务器端:
socket:同上
bind:同上
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);
sockfd为套接字,buf为读取的目的缓冲区,len为缓冲区长度,from和fromlen为对端信息
客户端:
socket:同上
int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
s为套接字,msg为要发送的缓冲区,len为发送字节数,to和tolen保存要发送的对端的信息
判断大小端
++i和i++在性能上的区别:
i++在执行过程中产生了一个临时变量,而++i并没有。因此,在使用类似for循环这种要运用到自增时,推荐使用++i
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
申请512M的内存
一个bit位代表一个unsigned int值
读入40亿个数,设置相应的bit位
读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在
strcmp
int strcmp(const char *str1,const char *str2) { /*不可用while(*str1++==*str2++)来比较,当不相等时仍会执行一次++, return返回的比较值实际上是下一个字符。应将++放到循环体中进行。*/ while(*str1 == *str2) { assert((str1 != NULL) && (str2 != NULL)); if(*str1 == '\0') return 0; str1++; str2++; } return *str1 - *str2; }
重载,覆盖,隐藏
重载
重载是指同名函数具有不同的参数表。
在同一访问区域内声明的几个具有不同参数列表(参数的类型、个数、顺序不同)的同名函数,程序会根据不同的参数列来确定具体调用哪个函数。
对于重载函数的调用,编译期间确定,是静态的,它们的地址在编译期间就绑定了。
重载不关心函数的返回值类型。
函数重载的特征
相同的范围(同一个类中)
函数名字相同
参数不同
virtual关键字可有可无
覆盖
覆盖是指派生类中存在重新定义基类的函数,其函数名、参数列、返回值类型必须同父类中相应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同,当基类指针指向派生类对象,调用该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本。
函数调用在编译期间无法确定,因虚函数表存储在对象中,对象实例化时生成。因此,这样的函数地址是在运行期间绑定。
覆盖的特征
不同的范围(分别位于派生类和基类)
函数名字相同
参数相同
返回值类型相同
基类函数必须有virtual关键字。
重载和覆盖的关系
覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。
覆盖只能由一对方法产生关系;重载是两个或多个方法之间的关系。
覆盖要求参数列表相同;重载要求参数列表不同。
覆盖关系中,调用方法是根据对象的类型来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的。
隐藏
隐藏是指派生类的函数屏蔽了与其同名的基类函数。
如果派生类的函数与基类的函数同名,但参数不同,则无论有无virtual关键字,基类的函数都被隐藏。
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时基类的函数被隐藏。
隐藏的特征
必须分别位于基类和派生类中
必须同名
参数不同的时候本身已经不构成覆盖关系了,所以此时有无virtual关键字不重要
参数相同时就要看是否有virtual关键字,有就是覆盖关系,无就是隐藏关系
指针函数与函数指针:
指针函数:
指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
int *fun(int x,int y);
函数指针:
int (*fun)(int x,int y);
函数指针是需要把一个函数的地址赋值给它,有两种写法:
fun = &Function;
fun = Function;
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
计算浮点数的开方
我们知道这个值的大小一定在1到2之间,所以采用二分的思想,left=1,right=2,然后不断判断mid来计算right-left>eps(eps=1e-5),直到小于这个数截止。
double f(double x) { return x*x; } double calSqrt() { double mid,left=1,right=2; while(right-left>eps) { mid=(left+right)/2; if(f(mid)>2) right=mid; else left=mid; } return mid; }
union和struct的区别
1:共用体和结构体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 共用体只存放一个被选中的成员, 而结构体则存放所有的成员变量。
2:对于共用体的不同成员赋值,将会对其他成员重写, 原来成员的值就不存在了, 而对于结构体的不同成员赋值是互不影响的
3:内存分配不同:union的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小
union的对齐:
typedef union
{
char c[10];
int i;
}u22;
10个字节就足以装下c或i了,但是对i要对齐,因此实际上是12个字节。
最终联合体的最小的size也要是所包含的所有类型的基本长度的最小公倍数才行
为什么要内存对齐
1、平台原因(移植原因)
A 不是所有的硬件平台都能访问任意地址上的任意数据的;
B 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:
A 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
B 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
cpu读取数据时有一个粒度,比如当粒度为4时,如果不进行内存对齐,则当取2~5字节的数据时,要先取0~3,在取4~7,最后拼接起来。而如果对齐,则直接就能取到这四个字节,因为他们放在cpu刚好能一次取完的四个字节的位置
read的返回值
1、如果读取成功,则返回实际读到的字节数。这里又有两种情况:
一是如果在读完count要求字节之前已经到达文件的末尾,那么实际返回的字节数将 小于count值,但是仍然大于0;
二是在读完count要求字节之前,仍然没有到达文件的末尾,这是实际返回的字节数等于要求的count值。
2、如果读取时已经到达文件的末尾,则返回0。
3、如果出错,则返回-1。
可重入函数与不可重入函数
可重入函数
一个函数在执行的过程中被打断,然后会再被从头执行一次,执行完后,再回来把刚才没执行完的部分执行完。这就相当于嵌套的执行了。函数是公共代码,这样的执行是允许的。函数的执行可以被打断,打断之后还可以再从头执行,执行完后接着执行刚才没有执行的代码,然后第一次执行的代码(被打断的函数)执行结果还是正确的。也就是说,这个函数执行,无论中间把这个函数再嵌入执行多少遍,怎么嵌入,最终执行完都不会对函数内部功能/程序逻辑造成影响,并且执行的结果都是正确的,这样的函数就是可重入函数。
常用的可重入函数:
- 不要使用全局变量,防止别的代码覆盖这些变量的值。
- 调用这类函数之前先关掉中断,调用完之后马上打开中断。防止函数执行期间被中断进入别的任务执行。
- 使用信号量(互斥条件)。
总而言之:要保证中断是安全的。
不可重入函数
函数执行期间,被中断,从头执行这个函数,执行完毕后再返回刚才的中断点继续执行,此时由于刚才的中断导致了现在从新在中断点执行时发生了不可预料的错误。也就是说,如果函数在不同的地方/时序进行调用,会对函数的功能逻辑造成影响,这种函数就称为不可重入函数。
常见的不可重入函数:
- 使用了静态数据结构
- 调用了malloc和free等
- 调用了标准I/O函数
- 进行了浮点运算
malloc与free是不可重入的,它们使用了全局变量来指向堆区。标准I/O大多都使用了全局数据结构
TCP粘包,拆包
1.服务端分2次读取到了两个独立的包,分别是D1,D2,没有粘包和拆包;
2.服务端一次性接收了两个包,D1和D2粘在一起了,被成为TCP粘包;
3.服务端分2次读取到了两个数据包,第一次读取到了完整的D1和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为拆包;
粘包、拆包发生原因
发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法
通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
僵尸进程
危害:
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
避免:
孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
main之前的操作
1.设置栈指针
2.初始化static静态和global全局变量,即data段的内容
3.将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
4.运行全局构造器,估计是C++中构造函数之类的吧
5.将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数
extern C的作用
extern "C"的主要作用就是为了能够正确实现C++代码调用其它C语言代码。
加上extern “C”后,会指示编译器将这部分代码按C语言进行编译,而不是C++的。这是因为C++支持函数重载,因此,编译器在编译函数的过程中会将函数参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
extern "C"包含两层含义:
1. extern关键字告诉编译器其声明的函数和变量可以在本模块或其他模块中使用。
在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern生命。与extern对应的是static,static表明变量或函数只能在本模块中使用,因此,被static修饰的变量或函数是不可能被extern “C”修饰的。
2. “C”表示编译器在编译时会按照C编译器的方式进行编译。
地址空间布局
https://www.cnblogs.com/Joezzz/p/9803344.html
堆和栈的区别
1)管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存泄露
2)空间大小:一般来讲在32位系统下,堆内存可以达到3G的空间(4G有1G要给内核);而栈的最大容量是事先规定好的(例如,在VC6下面,默认的栈空间大小是1M,可修改)
3)碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低;对于栈来讲,则不会存在碎片,因为栈是先进后出的结构,一个内存块要从栈中弹出,在它上面的后进的内存块肯定要先被弹出
4)生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长;
5)分配方式:堆都是动态分配的;而栈有2种分配方式:静态分配和动态分配,静态分配是编译器完成的,比如局部变量的分配,动态分配由alloca函数(类似于malloc,专门在栈中申请空间的函数)进行分配,但是即使是动态分配,它也和堆是不同,栈的动态分配是由编译器进行释放,无需我们手工释放
6)分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高;堆则是C/C++函数库提供的,它的机制是很复杂,例如:为了分配一块内存,库函数会在堆内存中搜索连续的足够大小的空间,如果没有足够大的空间(可能是由于内存碎片太多),就需要操作系统重新整理内存空间,这样就有机会分到足够大的内存,然后进行返回。显然,堆的效率比栈要低得多
标准IO和文件IO的区别(fread和read的区别/fwrite和write的区别)
文件IO跟标准IO的区别
1、 通过系统IO读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。标准IO可以看成是在文件IO的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。
2、 文件IO中用文件描述符表现一个打开的文件,可以访问不同类型的文件,如普通文件、设备文件和管道文件等。而标准IO中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。
标准IO可以看成是在文件IO的基础上封装了缓冲机制:
读时:read是系统调用,每次要读几个字节就读几个字节。而fread则会多读一些字节到缓冲区,这样下次就不用调用系统调用去读了。比如读4byte,read就只会读4byte.而fread可能会读4K到自己的缓冲区,这样下次再要读4byte时,read就仍要去内核读,而fread则直接在自己的缓冲区中去读取。
写时:write也是系统调用,每次要写几个字节就写几个字节。而fwrite则会将内容先写到自己的缓冲区,到满了或者fflush时再写入内核缓冲区,这样相当于节省了很多次write系统调用。
malloc最大申请多大的内存空间
地址空间限制是有的,但是malloc通常情况下申请到的空间达不到地址空间上限。内存碎片会影响到你“一次”申请到的最大内存空间。比如你有10M空间,申请两次2M,一次1M,一次5M没有问题。但如果你申请两次2M,一次4M,一次1M,释放4M,那么剩下的空间虽然够5M,但是由于已经不是连续的内存区域,malloc也会失败。系统也会限制你的程序使用malloc申请到的最大内存。Windows下32位程序如果单纯看地址空间能有4G左右的内存可用,不过实际上系统会把其中2G的地址留给内核使用,所以你的程序最大能用2G的内存。除去其他开销,你能用malloc申请到的内存只有1.9G左右
Linux内存分配
https://blog.csdn.net/gfgdsg/article/details/42709943
两个一样的玻璃球,如果从相同的楼摔下去都会碎,假设选择有一到一百层楼,问用最少的次数判断玻璃球从哪一层下去会碎?
两个玻璃球,第一个玻璃球用于分段,第二个玻璃球用于找到具体的楼层:
比如,第一个球在35层碎了,则我们从第一层开始逐渐往上增加到35层,当某一层碎了,这一层就刚好是碎的那一层。
而如果在35层没有碎,则从36层逐渐往上增加,直到某一层碎了,则这一层刚好是碎的那一层。
因此第一个球实际上用来找到临界段,第二个球用于找到具体的层数。
现在的问题时,由于是求最坏时间复杂度,也就是说如果在第99层才碎,则要试99次。
现在要找到一个均匀分布的方法,使这个球无论在哪层碎,最终使用的次数都一样。
假设总共要试k次:
如果要试k次,则当第一个球在第k层碎掉,这里花了一次。第二个球在1~k-1层中找打具体的楼层的最坏复杂度为k-1,则总共的复杂度为k
而如果不在第k层碎掉,则还可以用第一个球去找临界段,但已经扔了一次了,因此如果还要保证最后为k,则应该从k层往上数k-1层。即在第k+k-1层去扔第一个球,这样此时:
第一个球在第k层扔一次花了1次,如果在第k+k-1层碎了,这时又花了1次,而第二个球在k+1~k+k-1共花了k-2次,则在第k+k-1层坏掉,也是1+1+k-2=k次。
同理,如果k+k-1层也没碎,则要在第k+k-1+k-2去试。。。。
最后要求k+k-1+k-2+...+2+1>=99。99是因为如果99都没碎,则一定在100层碎了。
解得k=14.
这样,如果在第14层碎了,花费了14次
而如果在第27层碎了,花费了:14层一次,27层一次,15~26共12次,1+1+12=14,这样也是14次。
以此类推,无论在14,还是27,还是39层碎,都只需要14次就能找到对应层。
哈夫曼树
https://blog.csdn.net/xueba8/article/details/78477892
KMP
pthread_join与pthread_detach
pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程
pthread_detach()即主线程与子线程分离,子线程结束后,资源自动回收
pthread有两种状态joinable状态和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)
C++哪些函数不能声明为虚函数
1)普通函数
普通函数不属于成员函数,是不能被继承的
2)友元函数
友元函数不属于类的成员函数,不能被继承
3)构造函数
首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。
4)内联成员函数
我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。
5)静态成员函数
首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。
内联函数、静态函数和普通函数之间的区别?
答:
1.内联函数和普通函数最大的区别在于内部的实现方面,当普通函数在被调用时,系统首先跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个拷贝; 而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开(很类似宏的使用),如果在 N处调用了此内联函数,则此函数就会有N个代码段的拷贝。
2.static函数和普通函数的最大的区别在于作用域方面,static函数限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说,可以被其它代码文件调用该函数。同时static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。
伙伴系统
如果分页来避免碎片,则效率过低:
比如如果分页大小为4KB,则对一个16KB的内存来说,每进行到下一个4KB的内存都会触发缺页错从而导致页面置换,这样16KB中的4个页都要进行MMU映射,效率太低。
Linux内存分配——伙伴系统
inode
https://www.cnblogs.com/cherishry/p/5885107.html
inode是什么?
理解inode,要从文件储存说起。文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”。每个扇区能储存512字节(相当于0.5KB)
操作系统在读取硬盘的时候,不会一个个扇区的读取,这样效率太低,而是一次性连续读多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4kb,即连续八个sector组成一个block。
文件数据都存放在block中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为“索引节点”
每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
inode的内容
inode包含文件的元信息,具体来说有以下内容:
- 文件的字节数
- 文件的拥有者uid
- 文件的所属组gid
- 文件的r、w、x权限
- 文件的时间戳
- ctime:文件的inode上一次变动的时间
- mtime:文件内容上一次变动的时间
- atime:文件上一次打开的时间
- 硬链接数
- 文件数据block的位置
总之,除了文件名以外的所有文件信息,都存在inode之中。至于为什么没有文件名,下文会有详细解释
inode号码
每个inode都有一个号码,操作系统用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或绰号。
表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分为三步:
- 系统找到这个文件名对应的inode号码
- 通过inode号码,获取inode信息
- 根据inode信息,找到文件数据所在的block,读出数据