美文网首页
Golang面试之Linux系统-进程/线程

Golang面试之Linux系统-进程/线程

作者: 8411e9740257 | 来源:发表于2020-05-08 21:54 被阅读0次

    参考

    进程

    进程process是指正在执行的程序;是程序正在运行的一个实例。它由程序指令,和从文件、其它程序中读取的数据或系统用户的输入组成。

    在 Linux 中主要有两种类型的进程:

    • 前台进程
      也称为交互式进程,这些进程由终端会话初始化和控制。换句话说,需要有一个连接到系统中的用户来启动这样的进程;它们不是作为系统功能/服务的一部分自动启动。

    • 后台进程
      也称为非交互式/自动进程,这些进程没有连接到终端;它们不需要任何用户输入。

      • 守护进程
        后台进程的特殊类型,它们在系统启动时启动,并作为服务一直运行;它们不会死亡。它们自发地作为系统任务启动(作为服务运行)。但是,它们能被用户通过 init 进程控制。

    当现有的进程在内存中完全拷贝一份自身的时候就会创建出一个新的进程。子进程会有和父进程一样的环境,只有进程 ID 不同。

    在 Linx 中有两种常规方式创建进程:

    • 使用 system() 函数 - 这个方法相对简单,但是比较低效而且具有明显的安全隐患。
    • 使用 fork() 和 exec() 函数 - 这个技巧比较高级但提供更好的灵活性、速度以及安全性。

    在执行过程中,取决于它的环境一个进程会从一个状态转变到另一个状态。在 Linux 中,一个进程有下面的可能状态:

    • Running
      此时它正在运行(它是系统中的当前进程)或准备运行(它正在等待分配 CPU 单元)。
    • Waiting
      在这个状态,进程正在等待某个事件的发生或者系统资源。另外,内核也会区分两种不同类型的等待进程;可中断等待进程interruptible waiting processes - 可以被信号中断,以及不可中断等待进程uninterruptible waiting processes- 正在等待硬件条件,不能被任何事件/信号中断。
    • Stopped
      在这个状态,进程已经被停止了,通常是由于收到了一个信号。例如,正在被调试的进程。
    • Zombie
      该进程已经死亡,它已经停止了但是进程表process table中仍然有它的条目。
    进程状态图

    线程

    线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

    进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    使用线程的好处

    从上面我们知道了进程与线程的区别,其实这些区别也就是我们使用线程的理由。总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

    使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

    使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

    除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

    • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
    • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
    • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

    线程缺点

    线程同样也有缺点,最大的缺点是线程的不安全性,缺乏保护机制。就是上面提到的黑科技,因为线程间共享数据,一个线程可以重写另外一个线程的堆栈,导致出现一些异常的情况。除此之外,线程还有以下缺点:

    • 共享属性:全局变量是在所有线程间共享的,访问时是需要同步加锁。
    • 很多库函数是线程非安全的,多线程编程时,需要注意这一点。
    • 线程的健壮性不强,如果一个线程crash了,那么整个应用程序就跪了。

    进程和线程

    1. 线程其实是共享了某一个进程的资源,这些资源包括:
      • 内存地址空间
      • 进程基础信息
      • 公有数据
      • 打开的文件描述符
      • 信号的处理器
      • 当前工作目录
      • 进程代码段
      • 进程用户ID与进程组ID
    2. 线程独自拥有
      • 线程ID
      • 寄存器组的值
      • 线程的堆栈
      • 栈的局部变量和返回地址
      • 错误码 errno
      • 线程的信号屏蔽码
      • 线程优先级

    进程间通信

    进程能够单独运行并且完成一些任务,但是也经常免不了和其他进程传输数据或互相通知消息,即需要进行通信,进程之间相互通信的技术--进程间通信(InterProcess Communication,IPC)。

    进程间通信的目的:

    1. 数据传输
      一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间。
    2. 共享数据
      多个进程想要操作共享数据,一个进程对共享数据。
    3. 通知事件
      一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
    4. 资源共享
      多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
    5. 进程控制
      有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

    linux下进程间通信的几种主要手段简介:

    1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
    2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
    3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
    5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
    6. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

    线程间通信

    线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。

    线程通信的方式主要有:

    1. 互排锁
    2. 读写锁
    3. 条件变量
    4. 信号量
    5. 管道

    CPU上下文切换

    Linux 是一个多任务操作系统,它支持同时运行的任务数量远大于 CPU 个数。其实这些任务没有真正的同时运行,是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。
    而在每个任务运行前,CPU 都需要知道任务从哪里加载、从哪里开始运行,需要系统事先设置好 CPU 寄存器和程序计数器。CPU 寄存器是 CPU 内置的容量小、速度极快的内存。而程序计数器则是用来存储 CPU 正在执行的指令位置、或即将执行的下一条指令位置。它们都是 CPU 在运行任务前必须依赖的环境,也被叫做 CPU 上下文。
    上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
    根据任务的不同,CPU 的上下文切换可以分为几个不同的场景,也就是:进程上下文切换、线程上下文切换、中断上下文切换。
    CPU 上下文切换是保证 Linux 系统正常工作的核心功能之一,一般情况下我们无需特别关注。
    过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈、虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。

    1. 进程上下文切换

      1. 用户空间与内核空间
        Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着 CPU 特权等级的 Ring 0 和 Ring 3。
      • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源。
      • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入内核中才能访问这些特权资源。
      • 进程既可以在用户空间运行,又可以在内核空间运行。在用户空间运行时被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。
      1. 系统调用
        从用户态到内核态的转变,需要通过系统调用来完成。比如查看文件时,需要执行多次系统调用:open、read、write、close等。系统调用的过程如下:

        1. 首先,把 CPU 寄存器里原来用户态的指令位置保存起来
        2. 为了执行内核代码,CPU 寄存器需要更新为内核态指令的新位置,最后跳转到内核态运行内核任务。
        3. 系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
        4. 所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。

        但系统调用的过程中并不会涉及虚拟内存等进程用户态的资源,也不会切换进程,这和平时说的进程上下文切换是不一样的:

        • 进程上下文切换,是指从一个进程切换到另一个进程运行
        • 系统调用过程中一直是同一个进程在运行

        因此,系统调用的过程通常称为特权模式切换,而不是上下文切换。

      2. 进程上下文切换
        进程是由内核来管理和调度的,进程的切换只能发生在内核态,因此进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
        因此进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
        保存上下文和恢复上下文的过程并不是免费的,需要内核在 CPU 上运行才能完成。据测试,每次上下文切换都需要几十纳秒到数微妙的 CPU 时间。特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间消耗在寄存器、内核栈、虚拟内存等资源的保存和恢复上,从而大大缩短了真正运行进程的时间。
        Linux 通过 TLB 来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其它处理器的进程。

      3. 进程上下文何时切换
        Linux 为每个 CPU 维护了一个就绪队列,将活跃进程按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。那么,进程在什么时候才会被调度到 CPU 上运行呢?
        进程执行完终止了,它之前使用的 CPU 会释放出来,这时再从就绪队列中拿一个新的进程来运行。
        为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片被轮流分配给各个进程。当某个进程时间片耗尽了就会被系统挂起,切换到其它等待 CPU 的进程运行。
        进程在系统资源不足时,要等待资源满足后才可以运行,这时进程也会被挂起,并由系统调度其它进程运行。
        当进程通过睡眠函数 sleep 主动挂起时,也会重新调度。
        当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
        发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。

    2. 线程上下文切换
      线程与进程最大的区别在于,线程是操作系统调度的最小单位,而进程是操作系统分配资源的最小单位。所谓内核调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。对于线程和进程我们可以这么理解:

      • 当进程只有一个线程时,可以认为进程就等于线程
      • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
      • 另外线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也时需要保存的。

      其实线程的上下文切换可以分为两种情况:

      • 前后两个线程属于不同进程。此时因为资源不共享,所以切换过程就跟进程上下文切换是一样的。
      • 前后两个线程属于同一个进程。此时虚拟内存是共享的,上下文切换时,虚拟内存这些资源保持不动,只需要切换线程的私有数、寄存器等不共享的数据。

      可以发现同进程内的线程切换,要比多进程间的切换消耗更少的资源,这也正是多线程代替多进程的一个优势。

    3. 中断上下文切换

    相关文章

      网友评论

          本文标题:Golang面试之Linux系统-进程/线程

          本文链接:https://www.haomeiwen.com/subject/vqtcnhtx.html