一.进程描述符:
linux支持多进程特性,可以最大化的使用cpu资源;用户可以在同一个cpu上运行多个用户程序。多进程的原理是:时钟中断触发进程调度程序,调度程序分时运行多个进程。这就要求每个进程能够保留现场信息(cpu现场、系统资源、调度信息等)。
linux使用进程描述符数据结构记录现场信息,然后基于进程描述符管理进程,包括进程的创建、调度、消亡等操作。
进程不仅仅是运行着的程序,还包括拥有的系统资源、当前cpu现场、调度信息、进程间关系等重要信息,记录这些现场信息的结构就是进程描述符task_struct(可以在include/linux/sched.h中找到定义)。
而每个进程都有一个进程描述符,记录以下重要信息:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
存在的问题:进程描述符与PCB又有什么关系?
答:无语了,pcb就是由task_struct所定义。
二.进程的虚拟地址空间:
注:
*text:代码段,存放程序的可执行命令。
*data:存放已初始化数据。
*BSS:存放未初始化数据。
*堆:用于扩展进程的线性地址空间。
*栈:包含所有已分配内存的局部变量,用作函数调用。
三.inode:
1、inode是什么?
理解inode,要从文件储存说起。文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。(index node 简:inode)
通常,文件系统磁盘空间中大约百分之一空间分配给了 inode 表。
2、inode的内容
inode包含文件的元信息,具体来说有以下内容:
* 文件的字节数
* 文件拥有者的User ID
* 文件的Group ID
* 文件的读、写、执行权限
* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
* 链接数,即有多少文件名指向这个inode
* 文件数据block的位置
可以用stat命令,查看某个文件的inode信息:
stat example.txt
3、inode号码:每个inode都有一个号码,操作系统用inode号码来识别不同的文件。
这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。
使用ls -i命令,可以看到文件名对应的inode号码:ls -i example.txt
四.硬链接:
一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。
ln命令可以创建硬链接:ln 源文件 目标文件
运行上面这条命令以后,源文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做"链接数",记录指向该inode的文件名总数,这时就会增加1。反过来,删除一个文件名,就会使得inode节点中的"链接数"减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。
这里顺便说一下目录文件的"链接数"。创建目录时,默认会生成两个目录项:"."和".."。前者的inode号码就是当前目录的inode号码,等同于当前目录的"硬链接";后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的"硬链接"。所以,任何一个目录的"硬链接"总数,总是等于2加上它的子目录总数(含隐藏目录),这里的2是父目录对其的“硬链接”和当前目录下的".硬链接“。
五、软链接
除了硬链接以外,还有一种特殊情况。文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。所以读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。
这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:"No such file or directory"。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。
ln -s命令可以创建软链接:ln -s 源文文件或目录 目标文件或目录
六、inode的特殊作用
由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。
1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。
第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。
实际问题
在一台配置较低的Linux服务器(内存、硬盘比较小)的/data分区内创建文件时,系统提示磁盘空间不足,用df -h命令查看了一下磁盘使用情况,发现/data分区只使用了66%,还有12G的剩余空间,按理说不会出现这种问题。 后来用df -i 查看了一下/data分区的索引节点(inode),发现已经用满(IUsed=100%),导致系统无法创建新目录和文件。
查找原因:/data/cache目录中存在数量非常多的小字节缓存文件,占用的Block不多,但是占用了大量的inode。
解决方案:
1、删除/data/cache目录中的部分文件,释放出/data分区的一部分inode。
2、用软连接将空闲分区/opt中的newcache目录连接到/data/cache,使用/opt分区的inode来缓解/data分区inode不足的问题:
ln -s /opt/newcache /data/cache
七.dentry:
所谓"文件", 就是按一定的形式存储在介质上的信息,所以一个文件其实包含了两方面的信息,一是存储的数据本身,二是有关该文件的组织和管理的信息。在内存中, 每个文件都有一个dentry(目录项)和inode(索引节点)结构,dentry记录着文件名,上级目录等信息,正是它形成了我们所看到的树状结构;而有关该文件的组织和管理的信息主要存放inode里面,它记录着文件在存储介质上的位置与分布。同时dentry->d_inode指向相应的inode结构。dentry与inode是多对一的关系,因为有可能一个文件有好几个文件名(硬链接)
struct dentry {
struct inode *d_inode; /* Where the name belongs to - NULL is
struct dentry *d_parent; /* parent directory */
struct list_head d_child; /* child of parent list */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
......
};
struct inode {
unsigned long i_ino;
atomic_t i_count;
umode_t i_mode;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
unsigned long i_blocks;
unsigned short i_bytes;
unsigned char _sock;
12
struct inode_operations *i_op;
struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
......
};
八.FILE结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
struct
file结构体定义在include/linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的
struct
file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,
struct
file的指针通常被命名为file或filp。
struct
file {
union
{
struct
list_head fu_list; 文件对象链表指针linux/include/linux/list.h
struct
rcu_head fu_rcuhead; RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
} f_u;
struct
path f_path; 包含dentry和mnt两个成员,用于确定文件路径
#define f_dentry f_path.dentry f_path的成员之一,当前文件的dentry结构
#define f_vfsmnt f_path.mnt 表示当前文件所在文件系统的挂载根目录
const
struct
file_operations *f_op; 与该文件相关联的操作函数
atomic_t f_count; 文件的引用计数(有多少进程打开该文件)
unsigned
int
f_flags; 对应于open时指定的flag
mode_t f_mode; 读写模式:open的mod_t mode参数
off_t f_pos; 该文件在当前进程中的文件偏移量
struct
fown_struct f_owner; 该结构的作用是通过信号进行I/O时间通知的数据。
unsigned
int
f_uid, f_gid; 文件所有者id,所有者组id
struct
file_ra_state f_ra; 在linux/include/linux/fs.h中定义,文件预读相关
unsigned
long
f_version;
#ifdef CONFIG_SECURITY
void
*f_security;
#endif
void
*private_data;
#ifdef CONFIG_EPOLL
struct
list_head f_ep_links;
spinlock_t f_ep_lock;
#endif
struct
address_space *f_mapping;
};
|
1
|
我们在进程中打开一个文件F,实际上就是要在内存中建立F的dentry,和inode结构,并让它们与进程结构联系来,把VFS(虚拟文件系统)中定义的接口给接起来。我们来看一看这个经典的图:
|
(然并卵,并没怎么看懂)
1
|
下图为多个进程打开同一文件的情况:
|
九.文件描述符:
1.概述
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。
2. 文件描述限制
在编写文件操作的或者网络通信的软件时,初学者一般可能会遇到“Too many open files”的问题。这主要是因为文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a | grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。在Web服务器中,通过更改系统默认值文件描述符的最大值来优化服务器是最常见的方式之一,
1
2
3
4
5
6
7
8
9
10
11
|
十.Linux程序的终止:
正常终止:
从main返回
调用
exit
:ISO C定义
调用_exit或_Exit:前者由ISO C定义,后者由POSIX.1定义
最后一个线程从其启动例程返回
最后一个线程调用pthread_exit
异常终止
调用
abort
:产生SIGABRT信号
接到某些信号
最后一个线程对取消请求做出响应
|
十一.fork函数:
调用fork函数可以创建子进程,子进程共享父进程的代码段,父子进程各有自己的数据(写时拷贝完成这个功能)。
fork的返回值:
返回值小于零则表明佛如看出错。
父进程中返回子进程的pid。
子进程中返回0。
由于有不同的返回值,fork函数可以可以用来进行代码的分流。
fork出错的原因有两种:
当前进程数已经达到系统规定的最大值,这时errno为EAGAIN。
系统内存不足,这时errno为ENOMEN。
fork与vfork的差别
1. fork要拷贝父进程的数据段;而vfork则不须要全然拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段
2. fork不正确父子进程的运行次序进行不论什么限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的运行次序才不再有限制。
看看以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include<stdio.h>
#include<unistd.h>
int
main()
{
pid_t id;
int
i = 0;
for
(;i<2;i++)
{
id = fork();
if
(id == 0)
printf
(
"%d child %d %d \n"
,i,getpid(),getppid());
else
printf
(
"%d father %d %d\n"
,i,getpid(),getppid());
}
return
0;
}
|
第一次创建了两个进程(一共两个进程),父进程和子进程,第二次(一共四个进程)父进程又创建了一个子进程,上一个子进程作为父进程也创建了一个子进程,如果 i 的值变大,结果会创建2的n次方个进程。