进程与线程的相关面试问题总概

1、进程与线程的概念

  • 狭义定义: 进程是正在运行的程序的实例

  • 广义定义: 进程是一个具有一定独立功能的程序员关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程的组成

  • 程序
    程序部分描述了进程执行需要完成的功能,是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
  • 数据
    数据部分包括程度执行时所需要的数据和工作区。这部分只能为一个进程所专用,例如文本区域 (text region)、数据区域(data region)、和堆栈(stack region)等。
  • 程序控制块
    程序控制块是进程存在的唯一标志。其主要内容包括:进程标识符(标明系统中的各个进程)、状态(说明当前进程当前的状态)、位置信息(指明程序及数据存在主或外存的物理位置)、控制信息(参数、信号量、消息等)等。

线程

  • 线程是操作系统能够运算调度的最小单元。它被包括在进程之中,是进程中的实际运作单位。
  • 一条线程的进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并不执行相同的任务。
  • 线程基本上不拥有资源,只有一点运行中必不可少的资源,如程序计数器,一组寄存器和栈,它可与同属一个进程的其他线程共享进程所拥有的的资源。
    线程分为用户级宣传和内核级线程两类。用户级线程不依赖于内核,创建,撤销和切换等都不利用系统调度来实现;而内核支持线程依赖于内核,都利用系统调度来创建,撤销和切换。

线程与进程的主要区别

  • 通过上面对进程和线程的描述,可以总结出其主要的区别在于;
  • 线程是调度和分配的基本单位,进程是拥有资源的基本单位
  • 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
  • 进程的独立性比线程好,每个进程都拥有独立的地址空间和其他资源,不允许其他进程的访问
  • 线程间的通信比进程更方便,因为在同一进程下的线程共享全局变量、静态变量等数据
  • 进程创建。销毁及切换的系统开销大于线程的创建、销毁及切换

2、linux进程上下文

1、什么是上下文

进程上下文
  • 进程上文
    其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
  • 进程下文
    其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。
中断上下文
  • 中断上文
    硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。)
  • 中断下文
    执行在内核空间的中断服务程序。

2、为什么要进行不同状态之间的切换

  • 在现在操作系统中,内核功能模块运行在内核空间,而应用程序运行在用户空间。
  • 现代的CPU都具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,其所拥有的资源也不同;
  • 在较低的级别中将禁止使用某些处理器的资源。Linux系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态),这个级别几乎可以使用处理器的所有资源,而应用程序运行在较低级别(用户态),在这个级别的用户不能对硬件进行直接访问以及对内存的非授权访问。
  • 内核态和用户态有自己的内存映射,即自己的地址空间。当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。
  • 进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。
  • 其理解为保存用户空间状态是上文,切换后在内核态执行的程序是下文。

3、什么情况下进行用户态到内核态的切换

  • 进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。
  • 中断上下文是由于硬件发生中断时会触发中断信号请求,请求系统处理中断,执行中断服务子程序。
4、中断上下文代码中注意事项

运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。所以中断处理程序代码要受到一些限制,在中断代码中不能出现实现下面功能的代码:
睡眠或者放弃CPU。

  • 因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。牢记:中断服务子程序一定不能睡眠(或者阻塞)。
  • 尝试获得信号量 ,如果获得不到信号量,代码就会睡眠,导致上一条中的结果。
  • 执行耗时的任务 中断处理应该尽可能快,因为如果一个处理程序是IRQF_DISABLED(/中断禁止/)类型,他执行的时候会禁止所有本地中断线,而内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。中断处理程序的任务尽可能放在中断下半部执行。
  • 访问用户空间的虚拟地址
    因为中断运行在内核空间。

3、多进程与多线程的区别

  • 多线程优点:
  • 无需跨进程边界;
  • 程序逻辑和控制方式简单;
  • 所有线程可以直接共享内存和变量等;
  • 线程方式消耗的总资源比进程方式好;
  • 多线程缺点:
  • 每个线程与主程序共用地址空间,受限于2GB地址空间;
  • 线程之间的同步和加锁控制比较麻烦
  • 一个线程的崩溃可能影响到整个程序的稳定性
  • 线程较多的时候调度是一个大的问题,CPU频繁切换线程,CPU消耗大,性能下降;
  • 多进程优点
  • 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
  • 通过增加CPU,就可以容易扩充性能;
  • 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
  • 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限很大
  • 多进程缺点
  • 逻辑控制复杂,需要和主程序交互;
  • 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算;
  • 多进程调度开销比较大;

4、线程安全

  • 一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
怎样实现线程安全
  • 原子操作
  • 多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。
  • 同步与锁
  • 为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
  • 同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
  • 二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。
  • 可重入
  • 一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
  • 过度优化
  • 在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。
  • 我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。
  • 在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。

5、堆栈和线程的关系?

首先了解堆和栈
  • 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
  • 是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是thread safe的。每个C++对象的数据成员也存在在栈中,每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。
堆栈与线程进程
  • 进程栈大小时执行时确定,与编译链接无关
  • 进程栈大小是随机确认的,至少比线程栈要大,但不会超过2倍
  • 线程栈是固定大小的,可以使用ulimit -a 查看,使用ulimit -s 修改
  • 一般默认情况下,线程栈是在进程的堆中分配栈空间,每个线程拥有独立的栈空间,为了避免线程之间的栈空间踩踏,线程栈之间还会有以小块guardsize用来隔离保护各自的栈空间,一旦另一个线程踏入到这个隔离区,就会引发段错误。
    栈大小描述

6、线程的创建与分离、线程的join_able时,操作系统为什么不自动释放线程的资源?

  • 每个进程创建以后都应该调用pthread_join 或 pthread_detach 函数,只有这样在线程结束的时候资源(线程的描述信息和stack)才能被释放.
  • pthread_craete()出来的线程,joinable或detached两者必占之一。如果是jionale的线程,那么必须使用pthread_join()等待线程结束,否则线程所占用的资源不会得到释放,会造成资源泄露。
  • 如果想创建一个线程,但又不想使用pthread_join()等待该线程结束,那么可以创建一个detached的线程。detached状态的线程,在结束的时候,会自动释放该线程所占用的资源。
  • BUG
  • 但pthread创建后没有明确说明必须调用 pthread_join 或 pthread_detach,若没有调用二者之一,则会导致内存泄漏, 如果你创建的线程越多,你的内存利用率就会越高, 直到你再无法创建线程,最终只能结束进程。
  • 解决方案
  • 线程里面调用 pthread_detach(pthread_self()) 或pthread_detach (pthread_id)这个方法最简单
  • 在创建线程的设置PTHREAD_CREATE_DETACHED属性
  • 创建线程后用 pthread_join() 一直等待子线程结束。

7、线程状态

  • 新建状态(New):新创建了一个线程对象。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
  • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  • 死亡状态(Dead):线程执行完了或者因遇到error或exception退出了run()方法,该线程结束生命周期。
    线程状态转移

8、进程地址空间

  • 操作系统在管理内存时,每个进程都有一个独立的进程地址空间,进程地址空间的地址为虚拟地址,对于32位操作系统,该虚拟地址空间为2^32=4GB。其中0-3G是用户空间,3G-4G是内核空间。但4G的地址空间是不存在的,也就是我们所说的虚拟内存空间。进程在执行的时候,看到和使用的内存地址都是虚拟地址
  • 操作系统通过MMU部件将进程使用的虚拟地址转换为物理地址。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址
  • 虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。
  • 内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。
  • 地址空间是一个非负整数地址的有序集合。{0, 1, 2,……}
    如果地址空间中的整数是连续的,那么我们说他是一个线性地址空间。在一个带虚拟内存的系统中,CPU从一个有N = 2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。一个系统还有一个物理地址空间,对应于系统中物理内存的M个字节。
  • 地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。这就是虚拟内存的基本思想。主存中的每字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
  • 虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。
    VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块来处理这个问题。每个虚拟页的大小为P=2^pz字节。类似的,物理内存被分割为物理页,大小也为P字节(物理页也被称为页帧)。

9、线程池

  • 线程池的概念
  • 线程池,是一种线程的使用模式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。
  • 这就是线程池最核心的设计思路,「复用线程,平摊线程的创建与销毁的开销代价」
  • 线程池的作用
  • 避免了线程的重复创建与开销带来的资源消耗代价
  • 提升了任务响应速度,任务来了直接选一个线程执行而无需等待线程的创建
  • 线程的统一分配和管理,也方便统一的监控和调优
  • 线程池的使用场景
  • 高并发、任务执行时间短的业务
  • 线程池线程数可以设置为CPU核数+1,减少线程上下文的切换。
  • 并发不高、任务执行时间长的业务要区分开看
  • 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
  • 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
  • 并发高、业务执行时间长 ?
  • 解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考并发不高、任务执行时间长的业务要区分开看。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

10、信号量同步与互斥的问题

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
  • 信号量可以分为几类
  • 二进制信号量(binary semaphore):只允许信号量取0或1值,其同时只能被一个线程获取。
  • 整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。
  • 记录型信号量(record semaphore):每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。
  • 信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于0,则访问被允许,计数器减1;如果为0,则访问被禁止,所有试图通过它的线程都将处于等待状态。
  • 计数器计算的结果是允许访问共享资源的通行证。因此,为了访问共享资源,线程必须从信号量得到通行证, 如果该信号量的计数大于0,则此线程获得一个通行证,这将导致信号量的计数递减,否则,此线程将阻塞直到获得一个通行证为止。当此线程不再需要访问共享资源时,它释放该通行证,这导致信号量的计数递增,如果另一个线程等待通行证,则那个线程将在那时获得通行证。
    动作\系统
动作\系统Win32POSIX
创建CreateSemaphoresem_init
等待WaitForSingleObjectsem _wait
释放ReleaseMutexsem _post
试图等待WaitForSingleObjectsem _trywait
销毁CloseHandlesem_destroy
  • 互斥量和信号量的区别

    1. 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
  • 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

  • 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

    1. 互斥量值只能为0/1,信号量值可以为非负整数。也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
    1. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

11、进程的终止

  • 有8种方式使进程终止(termination),其中5种为正常终止,它们是:
  1. 从main返回。
  2. 调用exit。
  3. 调用_exit或_Exit。
  4. 最后一个线程从其启动例程返回。
  5. 最后一个线程调用pthread_exit。
  • 异常终止有3种方式,它们是:
  1. 调用abort。
  2. 接到一个信号并终止。
  3. 最后一个线程对取消请求作出响应。

12、进程状态/僵尸进程/孤儿进程/守护进程

孤儿进程

  • 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

僵尸进程

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

  • 僵尸进程怎样产生的:

  • 一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。

  • 在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。

  • 怎么查看僵尸进程:

  • 利用命令ps,可以看到有标记为Z的进程就是僵尸进程。

  • 怎样来清除僵尸进程:

  • 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

  • 把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。

  • 注:僵尸进程将会导致资源浪费,而孤儿则不会

守护进程

  • 守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
  • 守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的守护进程就是在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。
  • 由于在Linux中,每一个系统与用户进行交流的界面(例如Xshell)称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。
  1. 调用fork(),创建新进程,它会是将来的守护进程.
  2. 在父进程中调用exit,保证子进程不是进程组长
  3. 调用setsid()创建新的会话区
  4. 将当前目录改成跟目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
  5. 将标准输入,标注输出,标准错误重定向到/dev/null

补充:

  1. 孤儿进程有危害吗?
  • 孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
  1. 僵尸进程有危害吗?
  • 僵尸进程危害场景:“擒贼先擒王”。
    例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程(父进程)之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。这就是守护进程的作用,如果发生大量的僵尸进程,守护进程就会查找其父进程,然后无情的kill掉!
  1. 为什么要尽量避免僵尸进程?
  • 首先要明白,僵尸进程不是活着的进程,可以说就是一个数据结构,它是已经完成的任务的进程,但是不是它完成任务后就会烟消云散的,他会留下一点东西,这个东西就是他的进程Id,他的结束状态等,为什么了留下这个东西呢?因为这个是用来向他的父进程报告自己的完成状况用的,想想父进程为什么会创建一个进程,是用来完成任务的,父进程需要知道子进程的完成情况,所有出现这样的机制,对于僵尸进程只有父进程自己可以清理掉,调用wait等命令。就可以了。但是父进程不清理咋办,那么就说明僵尸进程存在,浪费了进程Id,进程的id是一种有限资源,用一个少一个啊,所以如果大量的僵尸进程存在的话,解决方法可以是杀掉无良的爹,孩子就可以被收养了。所以说,系统中的进程数量是有限的,虽然僵尸进程占用的资源和内存都比较少,但是它却占领着数字,可能会导致系统无法再创建新的进程,因此及时清除僵尸进程很重要!

13、死锁

  • 死锁的定义:
  • 死锁是指两个或两个以上的进程在执行过程中,由于资源竞争或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法推进下去,此时称系统处于死锁状态,这些永远在互相等待的进程称为死锁进程。
  • 死锁的原因:
  1. 因为系统资源不足;
  2. 进程运行推进的循序不合适;
  3. 资源分配不当;
  • 死锁产生的四个必要条件:
  1. 互斥条件:进程(线程)所申请的资源在一段时间内只能被一个进程(线程)锁占用。
  2. 请求与保持关系:一个进程因请求资源而阻塞时,对已获得资源保持不变;
  3. 不可抢占条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;

处理死锁的方法:

  • 破坏保持条件:
  1. 所有进程在开始运行之前,必须一次性获得所有资源,如果无法获得完全,释放已经获得的资源,等待;
  2. 所有进程在开始运行之前,只获得初始运行所需要的资源,然后在运行过程中不断请求新的资源,同时释放自己已经用完的资源。    
    相比第一种而言,第二种方式要更加节省资源,不会浪费(因为第一种可能出现一种资源只在进程结束用那么一小下,但却从头到尾都被占用,使用效率极低),而且,减少了进程饥饿的情况。
  • 破坏不可抢占条件:
    说起来简单,只要当一个进程申请一个资源,然而却申请不到的时候,必须释放已经申请到的所有资源。但是做起来很复杂,需要付出很大的代价,加入该进程已经持有了类似打印机(或者其他的有必要连续工作的)这样的设备,申请其他资源的时候失败了,必须释放打印机资源,但是人家用打印机已经用过一段时间了,此时释放打印机资源很可能造成之后再次是用打印机时两次运行的信息不连续(得不到正确的结果)
  • 破坏循环等待条件:
  • 设立一个规则,让进程获取资源的时候按照一定的顺序依次申请,不能违背这个顺序的规则。必须按照顺序申请和释放,想要申请后面的资源必须先把该资源之前的资源全部申请,想要申请前面的资源必须先把该资源之后的资源(前提是已获得)全部释放
  • 破坏互斥条件:
  • 没法破坏,是资源本身的性质所引起的
  • 常用的解除死锁的三种方法
  • 抢占资源:从一个或者多个进程中抢占足够的资源,分配给死锁进程,用于解除死锁;
  • 终止(或撤销)进程:终止(或撤销)系统中的一个或者多个死锁进程,直至打破死锁循环环路,使系统从死锁中解除出来
  • 利用银行家算法避免死锁

14、写时拷贝技术

  • 写时复制技术:(copy-on-write)
  • 内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
  • 拓展
    写实拷贝技术

15、管道

  • 管道是Linux中很重要的一种通信方式,是把一个程序的输出直接连接到另一个程序的输入,常说的管道多是指无名管道,无名管道只能用于具有亲缘关系的进程之间,这是它与有名管道的最大区别。
  • 有名管道叫named pipe或者FIFO(先进先出),可以用函数mkfifo()创建。
  • 在Linux中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现为:
  • 限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux中,该缓冲区的大小为1页,即4K字节,使得它的大小不象文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。
  • 读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。
  • 注意:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。
  • 管道的结构
  • 在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
  • 管道的读写
  • 管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
    1. 内存中有足够的空间可容纳所有要写入的数据;
    2. 内存没有被读程序锁定。
  • 如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索 引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入 数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
    管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可 以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
  • 因为管道的实现涉及很多文件的操作,因此,当读者学完有关文件系统的内容后来读pipe.c中的代码,你会觉得并不难理解。
  • Linux 管道对阻塞之前一次写操作的大小有限制。 专门为每个管道所使用的内核级缓冲区确切为 4096 字节。 除非阅读器清空管道,否则一次超过 4K 的写操作将被阻塞。 实际上这算不上什么限制,因为读和写操作是在不同的线程中实现的。

16、库函数和系统调用接口区别

  • 系统调用
  • 系统调用指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。它通过软中断向内核态发出一个明确的请求。系统调用实现了用户态进程和硬件设备之间的大部分接口。
  • 库函数
  • 库函数用于提供用户态服务。它可能调用封装了一个或几个不同的系统调用(printf调用write),也可能直接提供用户态服务(atoi不调用任何系统调用)。
  • 常见系统调用和库函数
  • 常见系统调用
  • open, close, read, write, ioctl,fork,clone,exit,getpid,access,chdir,chmod,stat,brk,mmap等,需要包含unistd.h等头文件。
  • 常见库函数
  • printf,scanf,fopen,fclose,fgetc,fgets,fprintf,fsacnf,fputc,calloc,free,malloc,realloc,strcat,strchr,strcmp,strcpy,strlen,strstr等,需要包含stdio.h,string.h,alloc.h,stdlib.h等头文件。
  • 区别
  • 系统调用通常不可替换,而库函数通常可替换
  • 普通的库函数调用由函数库或用户自己提供,因此库函数是可以替换的。例如,对于存储空间分配函数malloc,如果不习惯它的操作方式,我们完全可以定义自己的malloc函数。
  • 系统调用通常提供最小接口,而库函数通常提供较复杂功能
    例如sbrk系统调用分配一块空间给进程,而malloc则在用户层次对这以空间进行管理。
  • 系统调用运行在内核空间,而库函数运行在用户空间
    因为系统调用属于内核,而库函数不属于内核。因此,如果当用户态进程调用一个系统调用时,CPU需要将其切换到内核态,并执行一个内核函数。
  • 内核调用都返回一个整数值,而库函数并非一定如此
    在内核中,整数或0表示系统调用成功结束,而负数表示一个出错条件。而出错时,内核不会将其设置在errno,而是由库函数从系统调用返回后对其进行设置或使用。
  • POSIX 标准针对库函数而不是系统调用
    判断一个系统是否与POSIX需要看它是否提供一组合适的应用程序接口,而不管其对应的函数是如何实现的。因此从移值性来讲,使用库函数的移植性较系统调用更好。
    系统调用运行时间属于系统时间,库函数运行时间属于用户时间
    调用系统调用开销相对库函数来说更大
    很多库函数本身都调用了系统调用,那为什么直接调用系统调用的开销较大呢?这得益于双缓冲的实现,在用户态和内核态,都应用了缓冲技术,对于文件读写来说,调用库函数,可以大大减少调用系统调用的次数。而用户进程调用系统调用需要在用户空间和内核空间进行上下文切换,开销较大。如此以来,库函数的开销也就会比直接调用系统调用小了。另外一方面,库函数同样会对系统调用的性能进行优化。
  • 总结
  • 系统调用与库函数有联系也有区别,但是通常情况下,会建议使用库函数,主要出于以下几个方面的考虑:
    1. 双缓冲技术
    2. 移植性
    3. 系统调用本身性能缺陷
函数库调用 VS 系统调用
函数库调用系统调用
在所有的ANSI C编译器版本中,C库函数是相同的各个操作系统的系统调用是不同的
它调用函数库中的一段程序(或函数)它调用系统内核的服务
与用户程序相联系是操作系统的一个入口点
在用户地址空间执行在内核地址空间执行
它的运行时间属于“用户时间”它的运行时间属于“系统”时间
属于过程调用,调用开销较小需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc典型的系统调用:chdir fork write brk;

17、冯·诺依曼计算机组成部分(五大部分)

  • 运算器
  • 控制器
  • 存储器
  • 输出设备 输入设备

18、进程间通信原因以及进程间通信方式

通信原因:

百度解释
为了提高计算机系统的效率.增强计算机系统内各种硬件的并行操作能力.操作系统要求程序结构必须适应并发处理的需要.为此引入了进程的概念。进程是操作系统的核心,所有基于多道程序设计的操作系统都建立在进程的概念之上。目前的计算机系统均提供了多任务并行环境.无论是应用程序还是系统程序.都需要针对每一个任务创建相应的进程。进程是设计和分析操作系统的有力工具。然而不同的进程之间.即使是具有家族联系的父子进程.都具有各自不同的进程映像。由于不同的进程运行在各自不同的内存空间中.一方对于变量的修改另一方是无法感知的.因此.进程之间的信息传递不可能通过变量或其它数据结构直接进行,只能通过进程间通信来完成。
并发进程之间的相互通信是实现多进程间协作和同步的常用工具.具有很强的实用性,进程通信是操作系统内核层极为重要的部分。

  • 进程间协作必要的数据传输(交换信息)。
  • 但进程通信根据交换信息量的多少和效率的高低,又把通信方式分为低级通信(只能传递状态和整数值)和高级通信(提高信号通信的效率,传递大量数据,减轻程序编制的复杂度)。其中高级进程通信分为三种方式:共享内存模式、消息传递模式、共享文件模式。
  • 低级通信
  • 由于进程的互斥和同步,需要在进程间交换一定的信息,故不少学者将它们也归为进程通信。只能传递状态和整数值(控制信息)。
    特点:传送信息量小,效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。
    编程复杂:用户直接实现通信的细节,容易出错。
  • 高级通信
  • 提高信号通信的效率,传递大量数据,减轻程序编制的复杂度。
    提供三种方式:
    1.共享内存模式
    2.消息传递模式
    3.共享文件模式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

quchen528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值