Java教程

网络知识 - 进程/线程/协程

本文主要是介绍网络知识 - 进程/线程/协程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

面试笔记 - 进程/线程/协程

一个好的后端程序员,要清楚的知道进程、线程和协程的关系,这也是面试中普遍遇到的知识点,知识这东西最怕的就是似懂非懂,又会又不会,真的懂得这个知识点的人虽然不能那么顺利的回答出来,但在大脑里一定有痕迹,所以练好基本功,这是向上进步的基石。

面试问题:你怎么理解 进程/线程/协程 ?

进程

程序和进程

计算机刚被发明的时候,只能跑单一的程序,后来冯·诺依曼,提出了存储程序的思想,各种各样不同功能的程序写好之后,和程序使用的数据一起存放在计算机的存储器中,然后计算机按照存储的程序逐条取出指令加以分析,并执行指令所规定的操作。

--

进程结构一般由 3 部分组成 : 代码段、数据段和堆梳段 。

  • 代码段是用于存放程序代码的数据,假如机器中有数个进程运行相同的 一个程序,那么它 们就可以使用同一个代码段。
  • 数据段则存放程序的全局变量、 常量和静态变量。 堆械段 中的钱用于函数调用,它存放着函数的参数、函数内部定义的局部变量 。
  • 堆械段还包括了 进程控制块( Process Control Block , PCB)。 PCB 处于进程核心堆梭的底部,不需要额外分 配空间 。

程序转化成进程的步骤:

① 内核将程序读入内存,为程序分配内存空间;
② 内核为该进程分配进程标识符(PID)和其他所需资源;
③ 内核为进程保存 PID 及相应的状态信息,把进程放到运行队列中 等待执行,程序转化为进程后就可以被操作系统的调度程序调度执行了 。

PID:每个进程在系统中都有唯一的一个ID标识它,这个ID就是进程标识符(PID)。 因为其
唯一性,所以系统可以根据它准确定位到一个进程。

进程的创建

进程的创建方式有两种:一种是操作系统创建,一种是 由父进程创建 。

在系统启动时,操作系统会创建一些进程,它们承担着管理和分配系统资源的任务,这 些进程通常被称为系统进程 。 系统允许一个进程创建新进程(即为子进程),子进程还可以创 建新的子进程,形成进程树结构 。

pstree -npu  #linux中查看进程树命令

过程可描述为: 0号进程一>l号内核进程一>l号内核线程一>l号用户.进程(init进
程)一>ge即进程一>shell 进程 。

进程的创建 ( fork()函数 )

Linux 系统允许任何一个用户进程创建一个子进程,创建成功后,子进程将存在于系统 之中,并且独立于父进程 。 该子进程可以接受系统调度,可以得到分配的系统资源 。 系统也 可以检测到子进程的存在,并且赋予它与父进程同样的权利 。

父进程与子进程:

除了0号进程(该进 程是系统自举时由系统创建的)以外, Linux 系统中的任何一个进程都是 由其他进程创建的 。 创建新进程的进程,即调用 fork()函数的进程就是父进程,而新创建的进程就是子进程。

fork()函数不需要参数,返回值是一个进程标识符( PID ) 返回值,有以下情况:

  • 对于父进程, fork() 函数返回新创建的子进程的 PID。
  • 对于子进程, fork() 函数返回0。
  • 如果创建出错,则如此 fork()函数返回 -1。

由于创建的新进程和父进程在系统看来是地位平等的两个进程, 运行机会也是一样的,故不能够对其执行先后顺序进行假设,先执行哪一个 进程取决于系统的调度算法。

事实上,子进程完全复制了父进程 的地址空间 的内容,包括堆枝段和数据段的 内容。 但是,子进 程并没有复制代码段,而是和父进程共用代码段。 这样做是合理的,因为子进程可能执行不同的流程 来改变数据段和堆拢段,因此需要分开存储父子进 程各自的数据段和堆枝段 。 但是代码段是只读的, 不存在被修改的问题,因 此代码段可以让父子进程 共享,以节省存储空间.

僵尸进程和孤儿进程

在 UNIX/Linux 中,正常情况下,子进程是通过父进程创建的 子进程在创建新的进程 。 子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候 结束 。 于是就产生 了孤儿进程和僵尸进程 。

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

僵尸进程,是指一个进程使用 fork创建子进程,如果子进程退出,而父进程并没有调 用 wait或 waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这 种进程称为僵尸进程 。 当一个进程完成它的工作终止之后,它的父进程需要调用 wait()或者 waitpid()系统调用取得子进程的终止状态 。

可以这样理解孤儿进程和僵尸进程的区别:孤儿进程是父进程已退出,而子进程未退出; 僵尸进程是父进程未退出,而子进程已退出 。

守护进程

守护进程是脱离于终端并且在后台运行 的进程 。 守护进程脱离于终端是为了避免进 程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息 所打断 。

守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或 等待处理某些发生的事件 。 守护进程常常在系统引导装入时启动,在系统关闭时终止 。 Linux 系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多 系统任务,例如作业规划进程 crond、 打印进程 lqd等(这里的结尾字母 d就是 Daemon 的 意思) 。

由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始 运行的进程都会依 附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关 闭时,相应的进程都会自动关闭 。 但是守护进程却能够突破这种限制,它从被执行时开始运 转,直到整个系统关闭时才退出 。 如果想让某个进程不因为用户或终端或其他地变化而受到 影响 ,那么就必须把这个进程变成一个守护进程。

创建守护进程的步骤:

  • (1) 创建子进程,父进程退出。 在 Linux 中如果父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一 个孤儿进程时,就会自动由 1 号进程( init)收养它,这样,原先的子进程就会变成 init进程 的子进程。
  • (2)在子进程中创建新会话。 这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常 重大。 在这里使用的是系统函数 setsid,在具体介绍 setsid之前,首先要了解两个概念: 进程 组和会话期。
  • (3)改变当前目录为根目录 。
  • (4)重设文件权限掩码。
  • (5)关闭文件描述符 。

进程中的通信(管道、消息队列、共享内存、信号量)

管道

已经介绍了父子进程之间并不共享数据段和堆械段,它们之间是通过管道进行通信的,管道符|
管道是一种两个进程间进行单向通信的机制 。 因为管道传递数据的单向性,管 道又称为半双工管道,管道的这一特点决定了其使用的局限性 。 管道是 Linux 支持的最初 UNIXIPC 形式之一,具有 以下特点:

  • 数据只能由一个进程流向另一个进程(其中一个读管道, 一个写管道);如果要进行 双工通信,则需要建立两个管道 。
  • 管道只能用于父子进程或者兄弟进程间通信,也就是说管道只能用 于具有亲缘关系的进程间通信 。

除了以上局限性, 管道还有其他一些不足,如管道没有名字(无名管道) ; 管道的缓冲区
大小是受限制的;管道所传输的是无格式的字节流等。 这就需要管道输入方和输出方事先约定 好数据格式 。

使用管道进行通信时,两端的进程向管道读写数据是通过创建管道时, 系统设置的文件 描述符进行的。 从本质上说,管道也是一种文件,但它又和一般的文件有所不同,可以克服 使用文件进行通信的两个问题,这个文件只存在内存中 。

通过管道通信的两个进程, 一个进程向管道写数据,另外一个从中读数据 。 写入的数据 每次都添加到管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据的 。

消息队列

消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核 中用来保存消息的队列,它在系统内核中是以消息链表的形式出现 。

消息队列跟有名管道有不少的相同之处,消息队列进行通信的进程可 以是不相关的进 程,同时它们都是通过发送和接收的方式来传递数据的 。 在命名 管道 中, 发送 数据用 write 函数,接收数据用 read 函数,则在消息队列中,发送数据用 msgsnd 函数, 接 收数据用 msgrcv 函数。 而且它们对每个数据都有一个最大长度的限制。

与命名管道相比,消息队列的优势在于:
1消息队列也可以独立于发送和接收进程而存 在,从而消除了在同步命名管道的打开和关闭时可能产生 的困难;
2可以同时通过发送消息 以避免命名管道的同步和阻塞问题,而不需要由进程自己来提供同 步方法 ;
3接收程序可以 通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

事实上,它是一种正逐渐被淘汰的通信方式, 完全可以用流管道或者套接 口的方式来取 代它,所以,建议读者忽略这种方式 。

共享内存

共享内存是在两 个正在运行的进程之间共享和传递数据的一种非常有效的方式。 不同进程之间共享的内存通 常安排在同 -段物理内存中 。 进程可以将同一段共享 内存连接到它们 自己 的地址空 间中,所 有进程都可以访问共享内存中的地址。

不过,共享内存并未提供同步机制,也就是说,在第一个进程对共享内存的写操作结束 之前,并无自动机制可以阻止第二个进程对它进行读取。 所以通常需要用其他的机制来同步 对共享 内存的访问 。

优点:使用共享 内存进行进程间的通信非常方便,而且函数的接口也简单,数据的 共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率 。 同时,它也不 像无名管道那样要求通信的进程有一定的父子关系 。

缺点:共享 内存没有提供同步的机制,这使得在使用共享 内存进行进程间通信时, 往往要借助其他的手段来进行进程间的同步工作。

共享内存是进程间通信的最快的方式,但是共享 内存的同步问题自身无法解决(即进 程该何时去共享内存取得数据,而何时不能取),但用信号量即可轻易解决这个问题 。

线程

进程在多数早期多任务操作系统中是执行工作的基本单元 。 进程是包含程序指 令和相关资源的集合,每个进程 和其他进程一起参与调度,竞争 CPU、内存等系统资源 。 每 次进程切换,都存在进程资源的保存和恢复动作,这称为上下文切换 。 进程的引人可以解决 多用户支持的问题,但是多进程系统也在如下方面产生了新的问题 : 进程频繁切换引起的额 外开销可能会严重影响系统性能 。 进程间通信要求复杂的系统级实现 。 在程序功能日趋复杂
的情况下,上述缺陷也就凸显出来 。

由此就演化出了利用分配给同一个进程的资源,尽量实现多个任务的方法,这也就引入了线程的概念 。

通过线程可 以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信 也更简单 。

多线程模型

各个函数就像是连在一根线上一样,计算机像 一条流水线一样执行各个函数中定义的操作。 这样的一个程序叫做单线程程序。

多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而 让多个函数的操作同时运行 。 即使是单核 CPU 的计算机,也可以通过不停地在不同线程的
指令间切换,从而造成多线程同时运行的效果 。

线程属性主要包括如下属性:作用域( scope)、梳尺寸( stack size)、 械地址( stack address)、 优先级( priority)、 分离的状态( detached state)、 调度策略和参数( scheduling policy and parameters)等。 默认的属性为非绑定、非分离、默认 lMB 大小的堆枝 、 与父进程 同样级别的优先级 。

多线程同步

多线程相当于一个并发系统,一般同时执行多个任务。 如果多个任务可以共享资源,特 别是同时写入某个变量的时候,就需要解决同步的问题。

互斥锁

互斥锁是一个特殊的变量,它有锁上( lock) 和打开 (unlock)两个状态。 互斥锁一般被 设置成全局变量 。 打开的互斥锁可以由某个线程获得 。 一旦获得,这个互斥锁会锁上,此后 只有该线程有权打开,其他想要获得互斥锁的线程, 会等待直到互斥锁再次打开的时候。

条件变量

互斥量是线程程序必需的工具,但并非是万能的。 它可能重复对互斥对象锁定和解锁,每次都会检查共享数据结构,以查找某个值。 但这是在浪费时间和资源,而且这种繁 忙查询的效率非常低 。

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常 和互斥锁一起使用。 使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解 开相应的互斥锁并等待条件发生变化。 一旦其他的某个线程改变了条件变量,它将通知相应 的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并重新 测试条件是否满足 。

信号量

线程还可以通过信号量来实现通信。信号量和互斥锁的区别: 互斥锁只允许一个线程 进入临界区,而信号量允许多个线程同时进入临界区 。

协程

协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。在swoole4中,已经实现了底层协程实现。

协程就是用户态的线程,这是swoole给出的对协程的定义。

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

这篇关于网络知识 - 进程/线程/协程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!