进程与线程 基础知识

小林coding的进程与线程章节
线程/进程和我们早先单线程的代码比起来,最大的变化就是他们都是混在一起运行,所以就要有一系列方法去掌握它们的运行顺序和数据交互

进程与线程定义

在这里插入图片描述
这个是单进程
播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候,Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放;
各个函数之间不是并发执行,影响资源的使用效率;

在这里插入图片描述
这个是多进程
进程之间如何通信,共享数据?
维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;

进程

进程包括正在跑的程序和正在管理它的内核信息。

用./运行程序时,内核会将代码放入内存的代码区,然后cpu在逐行执行的过程中将数据w往堆/栈分配。

进程的状态

我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)。

在这里插入图片描述
虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发

在这里插入图片描述
阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行
如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间。所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
Ctrl+Z 挂起进程;
阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;

进程终止的方式:
(1) 主线程的入口函数返回(推荐方式);
(2) 进程主动退出:一个线程调用ExitProcess;
(3) 进程被动终止:另一个进程调用了TerminateProcess;例如通过任务管理器中结束进程。
(4) 进程中的所有线程都终止了。
在这里插入图片描述
实时Linux系统就是可以在紧急状况下创建了进程之后不等内核来调度,非要立刻执行。
进程状态详解

进程的个人信息卡 PCB

在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的【PCB?好出戏啊】
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。【PCB就是一个结构体task_struct 啦】

PCB 具体包含什么信息呢?

进程描述信息:【打工人姓名和公司名】
进程标识符pid:标识各个进程,每个进程都有一个并且唯一的标识符;
用户标识符uid:进程归属的用户,用户标识符主要为共享和保护服务;

进程控制和管理信息:【打工人的当前工作状态和他的职称】
进程当前状态status :如 new、ready、running、waiting 或 blocked 等;
进程优先级:进程抢占 CPU 时的优先级;

资源分配清单:【打工人的工位和工作相关文件和他的技能】
有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
当前打开文件:用于记载进程正在使用的文件,通过它与内存文件管理表目建立联系,通过该表可以找到保存在外存中的文件。
消息队列指针:指向本进程从其他进程接收到的消息所构成消息队列的链头。
资源使用情况:记载该进程生存期间所使用的系统资源和使用时间,用于记账。
进程队列指针:用于构建进程控制块队列,它是系统管理进程所需要的。

CPU 相关信息:【打工人正在处理的具体内容和进度】
CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
包括通用寄存器、地址映射寄存器、PSW、PC。

CPU有n个PCB需要挨个调用的时候,一般会选择用链表把PCB组织起来,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。

  • 将所有处于就绪状态的进程链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列
  • 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

进程的控制

创建进程

内核创建进程的过程如下:

  • 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
  • 为该进程分配运行时所必需的资源,比如内存资源;
  • 将 PCB 插入到就绪队列,等待被调度运行;

因为每个进程都应当有自己的空间,但是分配下来又很费时,所以内核决定,等子进程需要读/写的时候,确切用到了数据内存的时候,才给分配空间并复制数据。——写时复制技术。
在这里插入图片描述
在这里插入图片描述

在程序中主要有三种进程创建的方法:
fork()、vfork()、clone()
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程【也有可能过继给别的进程接管】。
在这里插入图片描述

会打印8句printf

在这里插入图片描述
在linux调用fork函数,然后分别打开文件后,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。

Linux内核机制总结进程管理之进程与命名空间(一)【调用clone创建子进程时,使用标志位控制子进程是共享父进程的命名空间还是创建新的命名空间】

切换进程

可以通过 exec()函数来实现运行另一个新的程序。[进程ID并未改变]【exec后面带那些字母只是因为这几个exec的输入参数不同,他们的效果都是从当前进程切换到exec的输入参数里的进程】
在这里插入图片描述
exec函数时需要考虑哪些参数:
首先,要执行的新程序名(带不带路径);【既可以是绝对路径、也可以是相对路径】
其次,命令行参数(列表形式还是指针数组形式);
最后,环境表(要不要传递环境表)

函数上加的这个p其实表示的是PATH;execl()和execv()要求提供新程序的路径名,而execlp()和execvp()则允许只提供新程序文件名,系统会在由环境变量PATH所指定的目录列表中寻找相应的可执行文件,如果执行的新程序是一个Linux命令,这将很有用。
execv()的argv参数与execve()的argv参数相同,也是字符串指针数组;而execl()把参数列表依次排列,使用可变参数形式传递,本质上也是多个字符串,以NULL结尾。
在这里插入图片描述
这句的效果就是
/bin/ps $ ps -ef
因为是可变参数,所以必须要以NULL结尾。
还可以像下图那样
在这里插入图片描述
execve()函数会将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序。
进程控制之exec函数
exec系列函数(execl、execlp、execle、execv、execvp)使用【有测试的小例子】
【当初我刚从贪吃蛇转职到单片机的时候惊讶于程序变量居然会在我不主动赋值的情况下就会改变数值;
现在更恐怖了,程序有可能会跳走去执行另一个文件却不需要头文件,还能传些数据过去】


另外还有个system()函数
system()函数可以很方便地在我们的程序当中执行任意shell命令。在理论上,system()函数可应用于任何进程,并没有使用限制。

其主要优点在于使用上方便简单,代码灵活简洁,编程时无需自己处理对fork()、exec函数、waitpid()以及exit()等调用细节,system()内部会代为处理;
当然这些优点通常是以牺牲效率,增加系统开销为代价的,因为使用system()运行shell命令需要至少创建两个进程,一个进程用于运行shell、另外一个或多个进程则用于运行参数command中解析出来的命令,每一个命令都会调用一次exec函数来执行;

system也是调用了exec函数去执行一个系统命令,可以把system函数理解成对exec函数的一个包装。可是,光光包装起来,加个返回值,有可能吗?比如说把return语句在exec后面,根据exec系列的函数的特性,return语句肯定不会执行,那system函数到底是怎么实现的呢?实际上,system函数的具体执行步骤是这样的:

1.fork一个子进程;

2.在子进程中调用exec函数去执行command;

3.在父进程中调用wait去等待子进程结束。

创建出一个子进程,然后在子进程中用exec来执行命令,即是子进程成功执行了没返回也没关系,还有父进程可以返回嘛!

linux下execl和system函数

等待进程

在linux中,对于许多需要创建子进程的进程来说,有时需要监视子进程的终止时间以及终止时的一些状态信息。【就是说,之前不是exec()给切换去子进程了嘛,等切换出去的进程跑完了不还得回来接着跑父进程的嘛】系统调用wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息
在这里插入图片描述
让进程等着,可以选择接收信号 也可以NULL干等,可以指定等待某一个进程。成功则返回终止的子进程对应的进程号;失败则返回-1【如果没有子进程,就会返回-1】

僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号SIGKILL也无法将其杀死,因此,只能杀死僵尸进程的父进程(或等待其父进程终止),这样init进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用wait()将其回收,避免僵尸进程。

在这里插入图片描述
子进程睡5s退出之后主进程打印。
LInux:进程等待之wait() & waitpid()

使用wait()系统调用存在着一些限制,这些限制包括如下:

  • 如果父进程创建了多个子进程,使用wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
  • 如果子进程没有终止,正在运行,那么wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
  • 使用wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如SIGSTOP信号)而停止,或是已停止的子进程收到SIGCONT信号后恢复执行的情况就无能为力了。
  • 如果父进程中没有子进程,那么wait()总是返回-1
终止进程

进程终止的方式通常有多种,大体上分为正常终止和异常终止

  • 正常终止包括:
    main()函数中通过return语句返回来终止进程;
    应用程序中调用exit()函数终止进程;【exit() 函数会调用 _exit()
    应用程序中调用_exit()或_Exit()终止进程;
    _exit()函数和exit()函数的status参数定义了进程的终止状态,父进程可以调用wait()函数以获取该状态。
  • 异常终止包括:
    应用程序中调用abort()函数终止进程;
    进程接收到一个信号,譬如SIGKILL信号。

终止进程的过程如下:

  • 查找需要终止的进程的 PCB;
  • 如果处于执行状态,则立即终止该进程的执行,然后 CPU 运行其他进程;
  • 如果其还有子进程,则应将其所有子进程终止;
  • 将该进程所拥有的全部资源都归还给父进程或操作系统;
  • 将其从 PCB 所在队列中删除;

在这里插入图片描述
printf里面的内容没有加\n ,内容会保存在缓冲区,并不会当场输出。
用_exit(0)退出,不会清空缓冲区。
用exit(0)退出,会清空缓冲区,在程序退出的时候把hello打印出来。

【 _exit()直接进入内核,exit()则先执行一些清除处理(在进程退出之前要检查文件状态,将文件缓冲区中的内容写回文件)再进入内核。如果用_exit()函数直接将进程关闭,缓冲区的数据将会丢失。

阻塞进程

阻塞进程的过程如下:

  • 找到将要被阻塞进程标识号对应的 PCB;
  • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
  • 将该 PCB 插入到阻塞队列中去;

当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒

唤醒进程

进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。

如果某进程正在等待 I/O 事件,需由别的进程发消息给它,即只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。

唤醒进程的过程如下:

  • 在该事件的阻塞队列中找到相应进程的 PCB;
  • 将其从阻塞队列中移出,并置其状态为就绪状态;
  • 把该 PCB 插入到就绪队列中,等待调度程序调度;

上下文切换

在这里插入图片描述

CPU很忙,他要管一大堆PCB的事,所以要有CPU 寄存器来存实时数据,还要程序计数器来记录程序进度

做切换的是CPU,CPU除了要切换进程,还要切换线程和中断。所以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。

CPU 上下文切换的时候要保护好即将退场的进程的实时数据和程序进度指针。就是先把即将退场的任务的 CPU 上下文(CPU 寄存器和程序计数器里的数据)另外保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中。

进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

发生进程上下文切换有哪些场景?

  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

进程与进程之间是完全隔离的,进程A崩溃了完全不会影响到进程B,所以现在很多浏览器°都采用多进程的方式来实现,打开一个网页对应forkQ—个进程来执行。

进程组

linux内核之进程的基本概念(进程,进程组,会话关系)
在这里插入图片描述
进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。
除此以外,进程还有其他层次关系:进程、进程组和会话。
进程组和会话是为了支持shell作业控制而引入的概念。
当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
进程组的概念并不难理解,可以将人与人之间的关系做类比。一起工作的同事,自然比毫不相干的路人更加亲近。shell中协同工作的进程属于同一个进程组,就如同协同工作的人属于同一个部门一样。
进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。【进程包含于进程组,进程组包含于会话
引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。
shell中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。【所以每打开一个终端就是一个新的会话】
新进程默认继承父进程的进程组ID和会话ID。
但有了创建进程组的接口【int setpgid(pid_t pid, pid_t pgid); 】,新创建的进程组就不必继承父进程的进程组ID了。最常见的创建进程组的场景就是在shell中执行管道命令,代码如下:cmd1 | cmd2 | cmd3
下面用一个最简单的命令来说明,其进程之间的关系如图4-2所示。

ps ax | grep nfsd

ps进程和grep进程都是bash创建的子进程,两者通过管道协同完成一项工作,它们隶属于同一个进程组,其中ps进程是进程组的组长。

在这里插入图片描述

在这里插入图片描述图中通过管道创建了三个进程,
可以看到他们都属于一个进程组,其中组长进程是 11732,他们的父进程都是 1612
只要该进程组内有任何一个进程存留,那么该进程组都存在
linux内核之进程的基本概念(进程,进程组,会话关系)

守护进程

守护进程是在后台运行的一种特殊程序,它脱离于终端,从而避免被终端的信号所打断。
守护进程一般可以通过以下几种方式启动:系统启动脚本启动、inetd超级服务器启动、cron命令定时启动、nohup命令启动。
守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、并且进程也不会被任何终端所产生的信息所打断。
守护进程自成进程组、自成会话,即pid=gid=sid。

守护进程是一种很有用的进程。Linux中大多数服务器就是用守护进程实现的,譬如,Internet服务器inetd、Web服务器httpd等。同时,守护进程完成许多系统任务,譬如作业规划进程crond等。
守护进程Daemon,通常简称为d,一般进程名后面带有d就表示它是一个守护进程。

写日志守护进程

  1. 创建一个进程并把它老爸干掉,从而过继到init这个稳定的继父手上
  2. 关掉从父进程那里继承来的文件描述符,摆脱所有负累。
  3. 改变目录
  4. 不知道干嘛用的
  5. 守护进程的职责:写日志输出。
    在这里插入图片描述

在内核的rc文件里加上这个守护进程的路径,就能开机自启动了。

要关掉这个进程就用kill指令 $ sudo kill <进程号>

守护进程的三种实现方式
守护进程

线程

现在要告诉大家一个事情。
其实进程是不能用来运行代码的。每个进程都至少有一个线程,跑代码的是那个线程。
一个进程内可以有多个线程同时运行,多个同时运行的线程叫做线程组.
线程组中还会有一个主线程,它的线程ID等于该线程组的组ID。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
在这里插入图片描述

这样就能在共享视频数据的情况下,让线程1读取一点,线程2解压一点,线程3播放一点,然后再调度线程1,循环往复而且切换速度快【就是并发啦】。但是当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃,具体分析原因可以看这篇

所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;

线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位(包括内存、打开的文件等)。【线程是人力,进程是物力?】

线程相比进程能减少开销,体现在:

  • 线程创建和终止都比进程快,因为线程只需要创建和释放一些寄存器和栈。而进程需要PCB
  • 线程之间的切换比进程切换快,因为线程具有相同的地址空间(虚拟内存和全局变量共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。只需要切换线程的私有数据、寄存器、栈等不共享的数据
  • 线程之间的数据交互是共享的,而进程间的数据交互要经过内核。

用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。

线程池

创建线程需要 75k cycles (~20us)
启动线程需要 200k cycles (~60us)
唤醒线程需要 15k cycles (~5us)
线程池的好处在于不需要频繁的创建和销毁线程,复用创建好的线程执行任务,任务执行完成之后进入休眠状态,从而提升效率。

内核线程,轻量级进程,用户线程

内核线程只运行在内核态,不受用户态影响。

轻量级进程是内核支持的用户线程,是内核线程的抽象,每个轻量级进程都会与一个内核线程相关联,以此来实现由内核支持的用户线程。这个关联现在是NPTL来做的,实现了对POSIX标准的兼容。

用户线程从头至尾的一切工作如创建调度等等,都是独立于内核之外,仅在用户态下实现,内核并不支持。

所谓内核支持与否,可以根据我们上述的设计这样简单理解,创建线程之后能被加进内核的就绪队列然后被内核的调度器调度,那就说明内核是支持的。反之普通的用户线程,内核是看不到的,内核看到的就只是整个进程,也只会把进程加进就绪队列,调度进程之后用户态下的调度器再调度用户线程。

所以想要真正实现线程机制,内核线程是最基本的要求。内核支持的用户线程——轻量级进程与内核线程是单射关系,每个轻量级进程都有一个内核线程与之相对应,这就是常说的一对一模型。而普通用户线程的CPU调度实体还是进程,整个进程只对应的一个内核线程,即进程里面的多个用户线程也只对应一个内核线程,这就是多对一模型。

调度

不管什么操作系统,调度程序做的事情都可以归结为两步:

  1. 选一个线程
  2. 切换线程

什么时候会发生 CPU 调度

  • 当进程从运行状态转到就绪状态:
    时间片到啦,要换进程了。

  • 当进程从运行状态转到等待状态:
    进程跑着跑着突然说需要一个什么信号,就坐下了,嗯。。。

  • 当进程从等待状态转到就绪状态:
    假设有一个进程是处于等待状态的,但是它的优先级比较高,如果该进程等待的事件发生了,它就会转到就绪状态,一旦它转到就绪状态,如果我们的调度算法是以优先级来进行调度的,那么它就会立马抢占正在运行的进程,所以这个时候就会发生 CPU 调度。

  • 当进程从运行状态转到终止状态:
    进程跑完了!杀青!

操作系统之进程的状态(运行、就绪、阻塞、创建、终止)及转换(就绪->运行、运行->就绪、运行->阻塞、阻塞->就绪)

调度算法影响的是等待时间(进程在就绪队列中等待调度的时间总和),而不能影响进程真在使用 CPU 的时间和 I/O 时间。

五种调度原则

CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

进程调度算法

01 先来先服务调度算法
02 最短作业优先调度算法
03 高响应比优先调度算法
在这里插入图片描述
如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
04 时间片轮转调度算法
如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
如果设得太长又可能引起对短作业进程的响应时间变长。
一般来说,时间片设为 20ms~50ms 通常是一个比较合理的折中值。
05 最高优先级调度算法
静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
06 多级反馈队列调度算法
在这里插入图片描述
新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;

线程提速

线程能给程序运行提速的原理就在于,多个线程能够并行执行,如果只有一个CPU,那就是伪并行,也是能提速的。

举个例子来说明:
有A B两个进程,
A有三个线程,a,b,c
B只有它自己一个执行流。
所以调度时,就绪队列上 A 有三个结点能被调度,而B只有一个,这样算下来 A 运行的速度肯定比 B 快。

但要清楚如果只有一个CPU的情况下,由于线程切换的开销,多线程进程运行的总时间应该是多于单线程进程的。只是说开了多个线程就能够去抢占更多的时间片资源,运行的效率会更高。

使用多线程还有一点能够提速,就是遇到阻塞的情况,比如说 A 进程的 a 如果阻塞,bc两个线程还能正常运行。而如果是 B 运行到某个地方阻塞的话,B整个进程就阻塞了。当然这也是讨论内核支持的用户线程,如果是普通的用户线程,某个线程阻塞也会导致整个进程阻塞,因为内核是感觉不到多个线程。

操作系统——四种进程调度算法模拟实现(C语言)
一篇文章彻底弄懂进程和线程调度
进程线程调度方式

通信

通信,其实就是数据交互。交互,就是需要把数据放到一个对方能拿的地方,按一定的规则去拿。
系统里可以放数据的地方有 内存、磁盘、网络缓冲区

进程间通信。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
对于线程,因为它资源共享的,所以不怎么需要通信。主要也就是靠信号量去管理一下共享空间里的数据读写。

同时,也可以利用通信机制对进程进行阻塞

信号量、互斥量、条件变量、读写锁等可用于线程同步,他们都有对应的可以使之线程阻塞的方法。
C++线程同步——阻塞线程的方法
不要用while死等!!!不要用while死等!!!不要用while死等!!!
【休眠n秒】Windows的Sleep(毫秒)和linux的sleep(秒)、usleep(微秒)

管道

管道不适合进程间频繁地交换数据,但是它数据交互很直观。
要用管道通信,不是用管道连接两个进程,而是在一个进程里创建管道,然后fork一个新的进程,这样新进程里就会复制到源进程的管道的缓存地址。但是必须要一侧关掉读渠道,另一侧关掉写渠道,否则会出现数据混乱,所以就只能单向流动数据。要另一个方向就得有俩管道。

所谓的管道,就是内核里面的一串缓存。从管道的一端写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。无名管道用的是内核的内存空间,但是因为只有父子进程能拿到这个管道变量,所以只能用在父子进程里。

给管道创建并申请一个文件存储区域:申请内存空间 - 补充必要信息并创建inode - 将文件inode挂载进文件向量表 - 获得文件描述符 - 可以像文件一样使用write/read

pipe[2]是同一块文件区域的两个文件描述符。还记得当初学文件描述符的时候说到多个进程可以打开同一个文件 并且有他们各自的读写进度。

通过fork创建的管道是匿名管道,我们也可以像创建变量一样单独创建一个管道。对于命名管道,它可以在不相关的进程间也能相互通信,在进程里只要使用这个设备文件,就可以双向通信。通信数据都遵循先进先出原则

mkfifo()创建有名管道。它是像文件一样指定一个路径去开辟内存空间,这样的话 两个无关的进程就能通过路径来读取内容。但是这个路径下创建的文件本身不是用来保存已写未读数据的,它只是一个管道格式的空文件。就像纸杯电话这样?完美的比喻,而且纸杯电话也是半双工。

消息队列

消息队列是能按顺序存放消息的一段空间,输入的信息会被监听,如果达到要求就可以取出信息。
进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。【管道好像非要被读取了才会停止通信】

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。
消息队列里的消息并不一定是按顺序读取,也可以按照mtype把对应类型的信息挑选着读取出来。
而且消息队列的存储方式是哈希表,当队列里有1w条消息时,要读取的时候是先找到某个区域,然后再在对应区域里挨个搜索的。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。【那消息队列可以看作邮箱?匿名管道算随身包包?】
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
在这里插入图片描述

共享内存

C/C++ linux 实现共享内存(share memory)的读写操作【代码实例】
共享内存防止读写冲突
平时吧,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

而共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。【那你这不就跟管道差不多?而且也要防止冲突(所以要用信号量啦)】
说是共享内存、消息队列、信号量都是这样开辟空间之后使用的。用$ ipcs 这个指令可以查看系统当前的这些信息
在这里插入图片描述

在这里插入图片描述

在linux中,管道和共享内存有以下区别:

  • 管道需要在内核和用户空间进行四次的数据拷贝:由用户空间的buf中将数据拷贝到内核中 -> 内核将数据拷贝到内存中 -> 内存到内核 -> 内核到用户空间的buf。
    而共享内存则只拷贝两次数据:用户空间到内存 -> 内存到用户空间。【这是不是文件IO中的直接/非直接IO的应用啊。。】
  • 管道用循环队列实现,连续传送数据可以不限大小。共享内存每次传递数据大小是固定的;
  • 共享内存可以随机访问被映射文件的任意位置,管道只能顺序读写
  • 管道可以独立完成数据的传递和通知机制,共享内存需要借助其他通讯方式进行消息传递。

信号

给进程发信号都是要打断进程做点操作,所以信号的实现原理就是软中断。
【上面那个信号量属于标志位,这个信号属于一个指令】
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。信号事件的来源主要有硬件来源(如键盘 Cltr+C 终止该进程就是个信号)和软件来源(如 kill 命令)。信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程。我们可以为信号自定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。也可以选择忽略某个信号。

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号。
在这里插入图片描述
在这里插入图片描述

非实时信号,等同于不可靠信号,不支持排队;
实时信号,等同于可靠信号,支持排队。

Linux信号机制基本上是从Unix系统中继承过来的,早期不可靠信号的主要问题是:
进程每次处理信号后,就将对信号的响应重置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新安装该信号。

因此,Linux下的不可靠信号问题主要指的是进程可能对信号做出错误的反应以及信号可能丢失,因为这些不可靠信号阻塞的时候是不支持排队的,即未决信号集只有一个 0 或 1 的标记位,不能记录相关信号的触发次数。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。

进程处理信号的方式:
1.忽略此信号( SIGKILL,SIGSTOP不能被忽略)
2.执行用户所希望的动作
3.执行系统默认动作

信号处理

  1. 回调函数【由于每次只能发送一个信号,已经基本不怎么用了】
  2. 信号集合函数处理

【Linux】信号的处理以及信号集操作函数
进程间通信之信号集函数组

线程/进程崩溃

程序非法访问内存了,那程序会报 Segment Fault 错误(即段错误),然后一般会发送SIGSEGV这个信号。 C/C++ 里,收到这个信号就开始执行停机操作了。JAVA把这个信号的“回调函数”给自定义了,让它处理内存并正常运行然后发一句警告出来。

信号量

啧,中华文化明明有这么丰富的描述词汇,为什么要把signal和semaphore叫做信号和信号量。就因为有+1-1这样的操作就觉得【量】字就可以了吗,就觉得加一个量字就足够了吗?明明也可以叫旗语啊,而且既然是跟锁放在一起用的,你起名也跟锁沾点边啊!【好了,大家回忆起这段话应该就能记住这俩的差别了】

在这里插入图片描述
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。信号量其实就是队列,创建信号量的函数调用的就是创建队列的函数。

如果在串口中断把消息内容直接传给任务,就用队列传消息
如果在串口中断做个标记,任务里查询标记然后从串口缓冲区拿消息,就用信号量做标记

P操作和V操作

控制信号量的方式有两种原子操作:
一个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
【哦?P?停车场?看,前方有一个停车场,我们进去,空位就会-1!】
另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
在这里插入图片描述

保证共享内存在任何时刻只有一个进程在访问【互斥信号量】

具体的过程如下:
进程 A 在访问共享内存前,先执行了 P 操作(信号量减1),由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
若此时,进程 B 也想访问共享内存,执行了 P 操作(信号量减1),结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
直到进程 A 访问完共享内存,才会执行 V 操作(信号量加1),使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作(信号量加1),使信号量恢复到初始值 1。
可以发现,信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。

保证进程 A 应在进程 B 之前执行【同步信号量】

具体过程:
如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
可以发现,信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。

/*
线程 A 将会调用 foo() 方法,而
线程 B 将会调用 bar() 方法
以下程序确保 两线程按"foobar"顺序 被输出 n 次。

*/
typedef struct {
    int n;
    sem_t foo;
    sem_t bar;
} FooBar;

FooBar* fooBarCreate(int n) {
    FooBar* obj = (FooBar*) malloc(sizeof(FooBar));
    obj->n = n;
    sem_init(&(obj->foo),0,0);
    sem_init(&(obj->bar),0,1);
    //obj->n = n;
    return obj;
}

void foo(FooBar* obj) {
    
    for (int i = 0; i < obj->n; i++) {
        
        // printFoo() outputs "foo". Do not change or remove this line.
        sem_wait(&(obj->bar));
        printFoo();
        sem_post(&(obj->foo));
    }
}

void bar(FooBar* obj) {
    
    for (int i = 0; i < obj->n; i++) {
        
        // printBar() outputs "bar". Do not change or remove this line.
        sem_wait(&(obj->foo));
        printBar();
        sem_post(&(obj->bar));
    }
}

void fooBarFree(FooBar* obj) {
    sem_destroy(&(obj->bar));
    sem_destroy(&(obj->foo));
    free(obj);
}

Socket 通信

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,Socket 通信也行,不过Socket 通信还能跨网络与不同主机上的进程之间通信。
可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。【具体流程不是很理解。先放着吧】

信号量可以用于互斥和同步,锁用来互斥
信号量与锁的差别

任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。即,锁就是临界区的边界。
【我是看了后面章节对P和V的应用之后才理解下面这些内容的。之前看,看不懂什么用,之后开始纳闷为什么一个P函数就能阻塞进程了,才想起来这段】
根据锁的实现不同,可以分为「忙等待锁」和「无忙等待锁」
「忙等待锁」靠 while(锁); 来占线,也被称为自旋锁(spin lock)。
这是最简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。在单处理器上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
「无等待锁」就是获取不到锁的时候,不用自旋。
既然不想自旋,那当没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把 CPU 让给其他线程执行。

锁的种类

(1)互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。
(2)读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁
(3)自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。 【适用于那些只需要等几纳秒的情况】
(4)条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用)
(5)信号量。

悲观锁、乐观锁

后面提到的互斥锁、自旋锁,都是属于悲观锁。
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
【不行,别人肯定会瞎搞的,我做事的时候必须得护住】
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
【哎呀,我搞我的,他搞他的,有什么关系,出事了再看嘛】
在线文档和git就是应用的乐观锁。
乐观锁虽然去除了加锁解锁的操作【乐观锁跟没锁似的,所以也叫无锁编程】,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。;

锁的应用

两个线程都在等待对方释放锁。
定位死锁问题时,我们可以多次执行 pstack 命令查看线程的函数调用过程,多次对比结果,确认哪几个线程一直没有变化,且是因为在等待锁,那么大概率是由于死锁问题导致的。

锁的种类有很多,每种锁的加锁开销以及应用场景也可能会不同。
互斥锁和自旋锁,它俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现。

高并发的场景下,如果选对了合适的锁,则会大大提高系统的性能,否则性能会降低。如何用好锁,也是程序员的基本素养之一了。为了选择合适的锁,我们不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率

互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。【有两次线程上下文切换的成本】
线程上下切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。
所以,如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁【憋走!马上就好!】

多线程冲突与合作

操作系统也为每个进程创建巨大、私有的虚拟内存的假象,这种地址空间的抽象让每个程序好像拥有自己的内存,而实际上操作系统在背后秘密地让多个地址空间「复用」物理内存或者磁盘。

互斥

在这里插入图片描述

比如在使用全局变量的时候,需要把数值从内存中取到寄存器里,再改寄存器里的值,然后存回内存。这期间要是被别的线程进来改了内存里的值就乱套了。我们希望这段代码是互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区。

同步

所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
同步就好比:「操作 A 应在操作 B 之前执行」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执行」等;
互斥就好比:「操作 A 和操作 B 不能在同一时刻执行」;

在这里插入图片描述

妈妈一开始询问儿子要不要做饭时,执行的是 P(s1) ,相当于询问儿子需不需要吃饭,由于 s1 初始值为 0,此时 s1 变成 -1,表明儿子不需要吃饭,所以妈妈线程就进入等待状态。

当儿子肚子饿时,执行了 V(s1),使得 s1 信号量从 -1 变成 0,表明此时儿子需要吃饭了,于是就唤醒了阻塞中的妈妈线程,妈妈线程就开始做饭。

接着,儿子线程执行了 P(s2),相当于询问妈妈饭做完了吗,由于 s2 初始值是 0,则此时 s2 变成 -1,说明妈妈还没做完饭,儿子线程就等待状态。

最后,妈妈终于做完饭了,于是执行 V(s2),s2 信号量从 -1 变回了 0,于是就唤醒等待中的儿子线程,唤醒后,儿子线程就可以进行吃饭了。

生产者-消费者问题

生产者在生成数据后,放在一个缓冲区中;消费者从缓冲区取出数据处理;【需要同步】
任何时刻,只能有一个生产者或消费者可以访问缓冲区;【需要互斥】

在这里插入图片描述
【原来临界区只保护生成数据这部分啊,我还以为锁要放在最前面和最后面。

读者-写者问题

「读-读」允许:同一时刻,允许多个读者同时读
「读-写」互斥:没有写者时读者才能读,没有读者时写者才能写
「写-写」互斥:没有其他写者时,写者才能写

在这里插入图片描述
临界区真的很小诶。不过既然阻塞了写者,确实就不会被打断了,确实不需要临界区保护。哦对了,如果读数据的行为包在了临界区里,那是不是就不能有两个读者一起读了。

上面的这种实现,是读者优先的策略,因为只要有读者正在读的状态,后来的读者都可以直接进入,如果读者持续不断进入,则写者会处于饥饿状态。
那既然有读者优先策略,自然也有写者优先策略:
只要有写者准备要写入,写者应尽快执行写操作,后来的读者就必须阻塞;
如果有写者持续不断写入,则读者就处于饥饿;
在这里插入图片描述
临界区既是保护也是限制啊。当初读者没有读者的临界区保护,写者就没办法阻塞读者的进程。现在有了临界区,那写者就只要P(读者的信号量)就可以阻塞了。
开始有多个读者读数据,它们全部进入读者队列,此时来了一个写者,执行了 P(rMutex) 之后,后续的读者由于阻塞在 rMutex 上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。
同时,第一个写者执行了 P(rMutex) 之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过 V(wDataMutex) 唤醒写者的写操作。
真厉害
方案三更巧妙。在读者优先的基础上加了一个互斥信号量就实现了公平。
在这里插入图片描述
写者:阻塞我?我来的时候占了位置!我写不了,新的读者也别想来插队!老读者你看你的,瞪什么瞪,看完走你。

扩展阅读

跳槽必问的Linux进程管理之进程调度与切换

多线程比多进程成本低,但性能更低。
多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车。
多线程是平面交通系统,造价低,但红绿灯太多,老堵车。
我们现在都开跑车,油(主频)有的是,不怕上坡下坡,就怕堵车。

多线程优点:无需跨进程边界; 程序逻辑和控制方式简单; 所有线程可以直接共享内存和变量等; 线程方式消耗的总资源比进程方式好;
多线程缺点:每个线程与主程序共用地址空间,受限于2GB地址空间; 线程之间的同步和加锁控制比较麻烦; 一个线程的崩溃可能影响到整个程序的稳定性; 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
多进程优点:每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系; 通过增加CPU,就可以容易扩充性能; 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多进程缺点:逻辑控制复杂,需要和主程序交互;需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
在Linux下编程多用多进程编程少用多线程编程

github里的一些简单例程

看Linux内核源码,不要太去纠结函数前面的输入正确性判断,
主要看逻辑核心用了什么结构体,输入/输出了什么消息,做了什么操作。
【b站视频】Linux应用开发进程间通信

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值