Linux系统

1. 文件系统:

一个文件由一个目录项,inode和数据区域块组成。

Linux文件系统的组织形式:

目录项:文件名和inode节点号

Inode:文件索引点,文件基本信息的存放地和数据块指针存放地

数据块:文件具体内容存放地

读取文件的过程:

1. 根据文件目录项的inode信息找到目录文件对应的数据块

2. 根据文件名从目录文件对应的数据块找到对应的inode节点信息

3. 从文件inode节点信息中找到文件内容所在数据块块号

4. 读取数据块内容

  虚拟文件系统VFS

        虚拟文件系统时一个抽象层,位于应用和具体文件系统之间,主要起适配的作用,向上提供了统一的文件访问接口,向下兼容不同的文件系统。

        VFS建立了具体应用程序和具体文件系统的联系,提供了统一的访问接口实现对具体文件系统的访问。

作用:

        1. 提供文件操作统一接口,如open,read, write, close等

        2. 屏蔽底层文件系统差异,可以通过相同接口访问不同类型文件

        3. 允许用户将不同文件系统挂载到指定的目录下,使得用户可以方便地管理不同地文件系统,并在需要时切换文件系统。

  inode节点    

        indeo节点中文名叫做索引节点,作用是为了在物理内存上找到文件块,inode中包含文件地基本西悉尼,文件位置,文件创建者,创建日期,文件大小等,输入stat指令可以查看某个文件地inode信息。

        硬盘格式化地时候,操作系统自动将硬盘划分为数据区和inode区,可以使用df命令查看硬盘分区地inode总数和已经使用地数量。

 查找文件地流程:

        系统找到文件名对应的inode号码,通过inode号码获得inode信息,根据inode信息找到文件数据所在的block读取数据。

  bootloader

       它的主要作用是初始化硬件设备、设置硬件参数,并加载操作系统内核。在嵌入式系统中,bootloader是硬件启动后第一个被执行的程序,它位于操作系统和硬件之间,起到桥梁的作用。


bootloader分为两个阶段:

        第一阶段使用汇编来实现,它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码; (需要初始化的硬件有,关闭WATCHDOG、关中断、设置CPU的速度和时钟频率、RAM初始化等)

        第二阶段则通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。()

第一阶段的工作内容:

  • 硬件设备初始化。
  • 为加载Bootloader 的第二阶段代码准备RAM空间
  • 复制 Bootloader 的第二阶段代码到RAM空间中。
  • 设置好堆栈指针sp。
  • 跳转到第二阶段代码的C入口点。

第二阶段的内容:

  • 初始化本阶段要使用到的硬件设备。
  • 检测系统内存映射( memory map ). 确定版上使用多少空间,地址如何
  • 将内核映象和根文件系统映象从Flash 上读到RAM空间中。
  • ·为内核设置启动参数。 把参数放在约定好的地方,然后内核会获取。
  • 调用内核

内核的启动过程

        1. 进行全面的硬件初始化,这包括初始化 CPU、内存管理单元(MMU)、中断控制器、定时器等。

        2. 内存池初始化,初始化内存管理系统,创建和管理内核空间和用户空间的内存区域,同时配置和初始化虚拟内存管理,设置页表和内存映射。

        3. 进程管理,创建第一个进程init进程,负责启动和管理系统服务。

        4. 设备驱动初始化,加载和初始化设备驱动程序,根据设备树或其他配置文件来确定如何配置和管理硬件设备。

        5. 文件系统挂载,内核挂载根文件系统,并为用户空间提供文件系统接口,

        6. 启动用户空间,内核执行init进程,启动其他系统服务和进程,通过读取系统配置文件,启动后台服务,并设置系统环境。

        9. 启动用户程序,用户可以通过登录界面或其他方式启动应用程序。

bootloader、内核、根文件的关系 :

        bootloader完成处理器和外设的初始化后调用LINUX内核,Linux内核完成系统初始化后需要挂载某个文件系统作为根文件系统,然后加载必要的内核模块,启动应用程序。

Linux软链接和硬链接

        硬链接(实体链接):透过文件系统的inode来产生新档名,而不是新档案。

        假如A是B的硬链接,则A的inode节点号与B的目录项中的节点号相同,即一个inode节点对应两个不同文件名,两个文件名指向同一个文件。删除其中一个对另一个没有影响,每增加一个文件名,inode节点上的链接数加一,每删除一个对应的文件名,inode节点上的链接数减一知道为0则inode节点和对应的数据块被回收。

        使用 ln 源文件 硬链接文件  创建硬链接

  注意:1.不能对目录创建硬链接

             2.  不能对不同的文件系统创建硬链接

             3. 不能对不存在的文件创建硬链接

        软链接(符号链接):可以看作windows中的快捷方式,让你快速链接到目标档案或目录。

        A是B的软链接,A和B目录项中的inode节点号不同,指向两个不同的数据块,但是A的数据块中存放的是B的路径名,可以根据这个找到B的目录项,如果删除了B,A任然存在但是指向了一个无效链接。

        使用 ln -s 源文件 软链接文件

注意:

        1.可以对目录创建软链接

        2. 可以跨文件系统

        3. 可以对不存在的文件创建软链接

区分:

        如果源文件从一个目录移动到其他目录,访问软链接文件,系统就找不到了,而硬链接没有这个缺陷,想怎么移动就怎么移动;软链接需要系统分配额外的空间用于创建新的索引节点和保存源文件的路径。

文件IO相关流程

使用fopen函数打开一个文件,返回一个FILE *fp,这个指针指向的结构体有三个重要成员。

文件描述符:通过文件描述可以找到文件的inode,通过inode可以找到对应的数据块

文件指针:读或写共享一个文件指针,读或写都会引起文件指针的变化

文件缓冲区:读或写回先通过文件缓冲区,主要目的是为了减少磁盘的读写次数,提高读写磁盘的效率。

文件读的基本流程:

1. 进程调用库函数向内核发起读文件请求

2. 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项。

3. 调用该文件可用的系统调用函数read()

4. read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode。

5. 在inode中通过文件内容偏移量计算出要读取的页。

6.通过inode找到文件对应的address_space

7. 在address_space中访问该文件的页缓存树,查找对应的页缓存节点

  1. 如果页缓存命中,那么直接返回文件内容

  2. 如果页缓存缺失,那么产生一个缺页异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页,重新进行第六步查找页缓存

8. 文件读取成功。

文件写的基本流程:

1. 进程调用库函数向内核发起读文件请求

2. 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项。

3. 调用该文件可用的系统调用函数write()

4. read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode。

5. 在inode中通过文件内容偏移量计算出要写入的页。

6. 如果页缓存命中,直接把文件内容修改更新在页缓存的页中,写文件就结束了,这时文件修改位于页缓纯,没有写回磁盘中。

7.如果页缓存缺失,那么产生一个缺页异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页命中,进行第六步。

8. 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块,有两种方式可以把脏页写回到磁盘:

  1. 手动调用sync()或者fsync()系统调用把脏页写回

  2. b.pdflush进程会定时把脏页写回到磁盘

同时注意:脏页不能被换出内存,如果脏页正在被写回,那么会设置写回标记,这时候该页就会被上锁,其他写请求被阻塞直到锁释放

 mmap内存映射:

        mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

作用:

1.将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

内存映射的步骤

1. 用open打开文件,返回文件描述符

2. 用mmap建立内存映射,并返回映射首地址指针start

3. 对映射进行各种操作,显示(printf),修改(sprintf)

4. 用munmap(void *start, size_t lenght)关闭内存映射.

5. 用close系统调用关闭文件fd.

内存映射的实现过程

1. 进程启动映射过程,在虚拟地址空间中为映射创建虚拟映射区域,在当前进程的虚拟空间中找一段空闲的满足要求的连续的虚拟地址。

2. 调用内核空间的系统调用函数mmap,实现文件物理地址和进程虚拟空间地址的一一映射关系。通过文件描述符表中找到对应的文件描述符,然后在目录项中到对应的inode,通过inode定位到具体的文件磁盘物理地址,建立页表实现文件地址和虚拟地址区域的映射关系。

3.进程发起对这片映射区域的访问,引发缺页异常,实现文件内容到物理内存的拷贝。 读页面的时候,如果缓存空间中有该页则直接读取,没有就发生缺页异常。写的时候没有就置换页面到内存,然后记好标记,操作系统会把修改过的脏页自动更新回磁盘地址里。

mmap文件映射和普通读写有什么区别:

        常规文件读写需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被进程直接寻址,所以还需要将页缓存的数据页再次拷贝到用户空间,这样需要两次数据拷贝才能完成对文件内容的获取任务,写操作也是。

        而内存映射,在创建新的虚拟地址映射空间和建立虚拟内存和物理地址的对应关系并没有拷贝操作,在读写的时候发现内存中无数据而发起的缺页异常过程,将磁盘数据传入内存的用户空间,供进程使用,这里只用了一次数据拷贝。

        总的来说,mmap由于省了从内核空间到用户空间的数据拷贝,所以效率更高。

mmap可以实现匿名内存映射完成父子进程之间的通信

        在父进程先使用mmap(),需要flag指定为匿名映射(MAP_ANONYMOUS),且输入fd为-1,然后fork创建子进程,那么子进程也会继承mmap()返回的地址,那么父子进程可以通过映射区域进行通信。

匿名内存映射与匿名管道的区别

        匿名内存映射是创建一个共享内存区域供父子进程所访问,而匿名管道提供了一个数据通道,允许具有亲缘关系的进程(通常是父子进程)以半双工模式(一个读,一个写)进行通信。

        匿名内存映射适合的传输是双向的,且适合大数据量传输,当然他的读取需要用户手动同步,一个进程结束通常不会影响另一个进程,且由于他是内存级别的操作,很容易造成安全问题,而匿名管道是基于内核中的缓冲区来实现的,数据在写入时被复制到内核缓冲区,读取时从内核缓冲区中取出,他的传输数据相对较小传输时单向的,读写进程只要有一个结束管道都会被关闭。匿名

2. Linux进程的内存空间分配

        Linux创建进程后,操作系统会分配4G的虚拟地址,其中高地址中的1G分配给内核空间,低地址3G(0X00000000~0XBFFFFFFF)分配给用户空间.

linux内核空间里面的内容:

        进程管理(PCB结构体):包含了进程的状态信息,如进程ID,进程状态,程序计数器,寄存器集合,调度信息,涉及到文件IP操作的文件描述符等。

        内存管理单元:负责进程的内存分配和管理,包括虚拟内存的映射,内存页的分配与回收。

        系统调用接口:内核提供了一组系统调用供用户空间的程序调用以请求内核提供的服务,如文件操作,进程控制,网络通信等。

        虚拟文件系统:内核管理文件系统的访问,包括文件的创建、删除、读写等操作

        设备驱动管理:包含了各种硬件设备的驱动程序,用于管理硬件设备和操作系统之间的通信,提供了一些接口使用。


注意: 用户无法访问内核空间,否则会出现段错误

linux用户空间里面的内容:

        环境变量:进程的一些变量配置

        命令行参数: main函数的一些参数

        栈空间,地址从高到低,存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。

       内存映射段:在栈的下方是内存映射段,内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux的mmap()系统调用或WindowsCreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,所以它被用来加载动态库,如果

        堆空间:地址由低变高,存储动态内存分配,需要程序员手工分配,手工释放,实现方式为链表。

        BSS段:存储未被初始化的全局/静态变量

        数据段:存储已经初始化的全局/静态变量

        程序段:程序代码在内存中的映射,存放函数体的二进制代码,同时可能存储常量字符串。

        

cpu 进程运行的两种状态 : 用户态到内核态

如何转换

        用户态-> 内核态:通过中断、异常、陷入指令

        内核态-> 用户态: 设置程序状态PSW

两者差别:

        用户态,进程所能访问的内存空间和对象受到限制,其占有的处理器资源是可被占有的。

        内核态:能访问所有的内存空间和对象,且所占的处理器是不允许被抢占。

用户态到内核态的切换:

1. 系统调用:操作系统提供给用户程序从用户态切换到内核态的一种方式,其原理还是操作系统给用户程序提供了一个中断接口,某些库函数如read,write,fork等可以通过系统调用进入内核态从而完成实际的IO操作。

2.异常:当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

3.外围设备的中断:外围设备完成用户请求后向CPU发送中断信号,这时CPU会转去执行与中断信号对应的处理程序。

        1是用户主动发起切换的,2和3是被动的。

进程创建的方式

1. 系统初始化创建了一些前台进程与用户交互,或者运行在后台的进程也被称为守护进程。

2. 系统调用创建,正在运行的进程发出系统调用如fork来创建新进程

3. 用户请求创建,启动程序等

进程的实现

        操作系统为了执行进程间的切换,会维护这一张表格,这张表就是进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括进程计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其它在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,保证该进程随后能再次启动,就像从未被中断过一样。

下标第一列内容与进程管理有关,第二列内容与存储管理有关,第三列内容与文件管理有关。

中断处理某个调度的过程

  1. 硬件压入堆栈程序计数器等

  2. 硬件从中断向量装入新的程序计数器

  3. 汇编语言过程设置保存寄存器的值

  4. 汇编语言过程设置新的堆栈

  5. C中断服务器运行(典型的读和缓存写入)

  6. 调度器决定下面哪个程序先运行

  7. C过程返回至汇编代码

  8. 汇编语言过程开始运行新的当前进程、

进程创建函数 fork

  1. 对于父进程, fork()函数返回新创建的子进程的ID ;

  2. 对于子进程,fork()函数返回0;

  3. 如果创建出错,则fork ()函数返回-1,子进程不被创建。

fork后父子进程的异同

父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录.....

父子不同处:进程ID、fork 返回值、父进程ID、进程运行时间。

        父子进程复制遵循写时复制,现在的Linux内核在fork()函数时往往在创建子进程时并不立即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制操作才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,然后继续后面的操作。

孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(zombie)进程,不能被kill终止,长期以往进程号会被消耗殆尽无法创建进程。

守护进程:运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件

fork 和 vfork 的区别

fork()  子进程拷贝父进程的数据段和代码段

vfork() 子进程和父进程共享数据段和代码段

他们的区别:

        fork() 产生的子进程会进行写时复制,且父子进程的执行顺序不确定。

        vfork()产生的子进程与父进程共享数据,他保证父进程在子进程退出和调用exec()之前不会被执行,如果子进程依赖父进程的进一步动作将会导致死锁。

3. 进程间通信

进程间通信简称IPC,本质时让不同进程看到同一份资源

进程间通信类别

1.管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2. 命名管道FiFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

3. 消息队列:消息队列是由信息的链表,存放在内核中并有消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

4.共享存储:共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但是多个进程都可以访问。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

5.套接字socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

6.信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

7. 信号量

3.1 管道

      管道可以分为匿名管道和有名管道,前者主要用于本地关联进程之间的通信,后者可以用于通用进程,管道通信是单向的,他实现的原理是让不同的进程看到相同的进程资源,但是他的通信是单向的,需要手动设置通道流向。


匿名管道由pipe函数创建并打开
命名管道由mkfifo函数创建,由open函数打开

3.2 消息队列

消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息

        消息队列克服了信号承载信息量少,管道只能承 载无格式字节流以及缓冲区大小受限等缺点。

消息队列的三个函数接口

3.3 共享内存

        共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

        共享内存的好处是效率高,因为进程可以直接读写内存不需要任何数据拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

        管道和消息队列写入时需要用户空间拷贝到内核空间的读写缓冲区,然后从内核空间的读写缓冲区,拷贝到具体的缓冲区,读取时则相反,从内核具体的缓冲区读取到内核空间的读写缓冲区,然后从内核空间拷贝到用户空间中。

        而共享内存只需要两次,一次从输入文件到共享内存区,另一次从共享内存到输出文件。

3.4 信号量:

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。


信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

支持信号量组。

3.5 信号

以上四种都是正常情况下的进程间通信方式,而对于异常情况下的工作模式,就需要用到信号的方式通知进程。
        信号是进程通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,那么就有这三种处理信号的方式

  • 执行默认操作:Linux对于每种信号都规定了默认操作。
  • 捕捉信号:可以将信号定义为一个信号处理函数,当信号发生时执行相应的函数。
  • 忽略信号:不做任何处理。需要注意的是,有两个信号无法忽略,即SIGKILLSEGSTOP,它们用于在任何时候中断或结束某一进程。

3.6 socket编程

套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。通过使用一些网络协议进行网络通信

服务端和客户端初始化socket,得到文件描述符
服务端调用bind,将绑定在IP地址和端口
服务端调用listen,进行监听
服务端调用accept,等待客户端连接
客户端调用connect,向服务器端的地址和端口发起连接请求
服务端accept返回用于传输的socket的文件描述符
客户端调用write写入数据,服务端read读取数据
客户端断开连接时会调用close,那么服务端read读取数据时就会读取到EOF,在处理完数据后,服务器端调用close表示关闭连接。

UDP不需要连接,所以不需要调用listen和connect,但UDP之间的交互需要IP地址和端口号,所以仍需要bind
对于UDP来说,不需要维护连接,也就没有所谓的客户端与服务端,只要有一个socket多台机器就可以任意通信
每次通信时,调用sendto和recvform都要传入目标主机的IP地址和端口

优缺点:

管道: 速度慢、容量有限,只有父子进程能通信

FIFO:任何进程间都能通信,但是速度慢

消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题,消息队列可以不再 局限于父子进程,而允许任意进程通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息 发送和接收之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便, 但是信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合。此种方法不太常用。

信号量:不能用来传递复杂信息,只能用来同步

共享内存:利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。共享内存块提供了在任意数量的 进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵 守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。

什么是缺页中断?

        当用户程序尝试访问某个虚拟地址,操作系统利用页表将虚拟地址转换为物理地址,如果虚拟地址对应的页面在内存缓冲区不存在则会发送缺页中断。

        缺页中断的流程:

                保护CPU现场

                分析中断原因,

                转入缺页中断处理程序进行处理

                恢复CPU现场,继续执行

        但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般 的中断存在区别:

        1. 在指令执行期间产生和处理缺页中断信号 2. 一条指令在执行期间,可能产生多次缺页中断 3. 缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。

4线程

 线程:它是CPU调度的基本单位,承担进程资源的一部分的基本实体。在linux中,线程其实本质上是进程,操作系统创建多个task_struct共享一个进程的资源,Linux中所谓的“线程”只是在被创建时clone了父进程的资源,因此clone出来的进程表现为“线程”,这一点一定要弄清楚。因此,Linux“线程”这个概念只有在打引号的情况下才是最准确的

线程优点:

        创建一个线程的开销要比进程的开销要小。

        与进程之间的切换相比,线程之间的切换的开销较小,线程之间的切换只需要切换线程的上下文,不需要更新页表,加载有效数据。
线程的资源:线程ID,一组寄存器,储存自己的上下文信息,栈,errno,信号屏蔽字,调度优先级。

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程之间同步的方式

1.临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意 时刻只允许一个线程访问共享资源,如果有多个线程试图访问共享资源,那么当有一个线程进入 后,其他试图访问共享资源的线程将会被挂起,并一直等到进入临界区的线程离开,临界在被释放 后,其他线程才可以抢占。

2. 互斥量:为协调对一个共享资源的单独访问而设计,只有拥有互斥量的线程,才有权限去访问系统 的公共资源,因为互斥量只有一个,所以能够保证资源不会同时被多个线程访问。互斥不仅能实现 同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。

3. 信号量:为控制一个具有有限数量的用户资源而设计。它允许多个线程在同一个时刻去访问同一个 资源,但一般需要限制同一时刻访问此资源的最大线程数目

4.事件:用来通知线程有一些事件已发送,从而启动后继任务的开始。

用户线程和内核线程

用户线程:指不需要内核支持而在用户程序中实现的,其不依赖于操作系统核心,应用进程利用线程 库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作 系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这 里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。

内核线程:由操作系统内核创建和撤销,内核维护进程及线程的上下文信息以及线程切换。一个内核线 程由于I/O操作而阻塞,不会影响其它线程的运行。

用户线程的优点:

        1.创建和销毁速度块,由于用户线程的管理和调度由用户空间的线程库完成,不涉及内核调用,因此创建和销毁线程的速度较快。

        2. 上下文切换开销小:上下文切换只涉及用户空间的数据,通常不需要保存和恢复内核状态,从而减少了上下文切换的开销。

        3. 灵活控制,自定义线程调度策略,通过用户空间的线程库进行灵活的调度和管理,线程库可以实现自定义的调度策略和调度算法。

        4. 资源消耗小,由于不需要系统调用,用户线程的资源消耗较少,适合创建大量线程的场景。
        缺点:

        1. 阻塞问题,如果一个用户线程阻塞,由于操作系统感知不到线程的存在他会阻塞整个进程。

        2.在多个cpu情况下,只能使用一个调度一个进程里面的一个线程


 

内核线程的优点:

  1. 多处理器系统中,内核能够并行执行同一进程内的多个线程

  2. 如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行。

  3. 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程

内核线程的缺点:

        创建和切换开销较大,由操作系统调度。


用户级线程和内核级线程的区别:

  1. 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。

  2. 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。

  3. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断

  4. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。

  5. 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

什么是僵尸进程、孤儿进程、守护进程

僵尸进程是 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子 进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

孤儿进程是因为父进程异常结束了,然后被1号进程init收养。

守护进程是创建守护进程时有意把父进程结束,然后被1号进程init收养

区分: 一个正常运行的子进程,如果此刻子进程退出,父进程没有及时调用wait或waitpid收回子进程 的系统资源,该进程就是僵尸进程,如果系统收回了,就是正常退出,如果一个正常运行的子进程,父 进程退出了但是子进程还在,该进程此刻是孤儿进程,被init收养,如果父进程是故意被杀掉,子进程做 相应处理后就是守护进程

如何实现线程池?

1.设置一个生产者消费者队列作为临界资源

2.初始化n个线程,并让其运行起来,加锁去队列取任务运行

3. 当任务队列为空的时候,所有线程阻塞

4.当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻 塞中的一个线程

为什么堆的空间是不连续的

        堆包含一个链表来维护已用和空闲的内存块,申请和释放许多小的块可能会产生如下状态,在已用块之间存在很多小的空闲块,进而申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的不连续的,不能满足申请的大小,这叫堆碎片。

        当旁边有空闲块的已用块被释放时,新的空闲块会与相连的空闲块合并成一个大的空闲块,这样就可以 有效的减少"堆碎片"的产生。

        堆分配的空间在逻辑地址上是连续的,但是在物理地址上是不连续的,如果逻辑地址空间上已经没有一段连续且足够大的空间,则分配内存失败。

内存碎片如何产生?

外部碎片:

        程序在运行事不断地请求和释放内存,导致内存中产生不规则地空闲块,空闲内存被分割成多个小块,形成碎片。

内部碎片:

        如果内存分配器分配固定大小地内存块,而程序实际请求地内存大小可能小于分配地块地大小,导致块内部存在未使用地空间。

        解决方法:合并相邻地空闲块(外部碎片)。建立内存池(减少内部碎片和外部碎片),每个块用于特定类型地内存请求,减少内存碎片并提高分配效率。内存压缩,将内存中活动对象移动在一起,释放地内存区域进行整理和压缩。

如何在应用层上减少内存碎片:

        实现内存池:

        创建一个内存池,创建两个链表,其中一个RequestMemory存储从malloc和new中申请来但未使用地内存,另一个ReleaseMemory保存使用完释放回来地固定大小地内存块,开始时先申请固定大小地内存块,8,16,32,128若干并连接到RequestMemory中,申请内存首先去ReleaseMemory中查找能容纳地最小内存,如果找不到就去RequestMemory中找,如果大于128字节则直接malloc申请,释放内存首先将链接链接到ReleaseMemory中,如果大于128字节就直接释放。

        内存池减少内存碎片,提高内存分配地效率。

什么是用户栈和内核栈?

        内核栈和用户栈是分别处于用户态和内核态使用地栈。

        1.内核栈是内存中属于操作系统空间的一块区域,其主要用途未:

             a.保存中断线程,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出;

             b.保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。

        2. 用户栈是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。

 用户栈和内核栈,为什么不能共用一个栈?

        a.如果只用系统栈,系统栈一般大小有限,用户程序调用次数可能很多,系统栈大小一般为中断优先级减1,如果15次子程序调用以后的子程序的参数,返回值,局部变量就不能保存,用户程序也不能正常运行。

        b.如果只用用户栈,系统程序需要在某种保护下运行,而用户栈在用户空间不能提供相应的保护措施。

产生死锁的原因是什么

        多个线程因为竞争资源而产生相互等待的现象,每个线程都无法推进下去。

原因:

        1.系统资源有限

        2.进程推进顺序不恰当

死锁的四个必要条件:

        互斥:一个线程占有资源,别人就不能够访问该资源

        占有并等待:一个线程占有资源,还需要获得需要其他进程释放的资源

        不可抢占:别人已经占有了某项资源,不能因为自己需要就抢占资源

        循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

死锁的解决方法:

        主要有预防死锁,避免死锁,检测与解除死锁

        预防死锁:

                1. 资源一次性分配,破坏请求并保持条件

                2. 可剥夺资源

                3. 资源有序分配法,给每类资源赋予一个编号,每一个线程按编号递增的顺序请求资源,释放则相反。

        避免死锁:

               于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前 预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则, 进程等待。其中最具有代表性的避免死锁算法是银行家算法。

        解除死锁: 当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有: 1. 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态; 2. 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除 为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

        

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值