操作系统进程线程(一)—进程线程协程区别、多进程多线程、进程调度算法、进程线程通信

进程线程协程区别

定义上

  • 进程:资源分配和拥有的基本单位,是调度的基本单位。运行一个可执行程序会创建一个或者多个进程;进程就是运行起来的程序
  • 线程:程序执行基本单位,轻量级进程。每个进程中都有唯一的主线程,主线程和进程是相互依赖的关系。
  • 协程:用户态的轻量级线程线程内部调度的基本单位

切换情况

  • 进程:操作系统切换;从用户态到内核态再到用户态;进程CPU环境(栈(内核栈)、寄存器、页表和文件句柄等)保存;新调度的进程CPU环境设置
  • 线程:操作系统切换;从用户态到内核态再到用户态;保存和设置程序计数器、少量寄存器和栈(内核栈)的内容
  • 协程:用户切换;一直在用户态;保存寄存器上下文和栈(用户栈)

拥有资源

  • 进程:CPU、内存、文件和句柄
  • 线程: 程序计数器、寄存器、状态字、栈 (线程栈和栈指针)。

同一线程共享堆、全局变量、静态变量、指针、引用、文件等,而独自拥有栈。线程不拥有系统资源,但是一个进程的多个线程可以共享隶属进程的资源。

  • 协程:自己的寄存器上下文和栈

并发性

  • 进程:不同进程实现并发,各自占有CPU实现并行
  • 线程:一个进程内部多个线程并发执行(最好和CPU核数相等)
  • 协程: 同一个时间只能执行一个协程

系统开销

  • 进程:开销很大,切换虚拟地址空间、内核栈、硬件上下文,CPU高速缓存失效、页表切换开销都很大
  • 线程:保存和设置少量寄存器内容,开销很小
  • 协程:直接操作栈则基本没有内核切换的开销,可以不加锁地访问全局变量,所以上下文切换非常快

通信

  • 进程: 需要借助操作系统
  • 线程:可以直接读写进程数据段(比如全局变量)来进行通信。
  • 协程:共享内存、消息队列

使用以及开销

  • 线程使用有一定的难度,需要处理数据一致性 的问题
  • 进程创建和销毁需要重新分配、销毁task_struct结构;线程创建和销毁只需要处理PC值、状态码、通用寄存器值、线程栈和栈指针即可。
  • Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。这个结构体存放在叫做任务列表的双向循环链表中。系统中每一个进程/线程的PCB都是由这个双向链表来管理的

一个进程可以创建多少个线程?

无用线程要及时销毁,不然过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响。
参考一个进程最多可以创建多少个线程?
在这里插入图片描述
这与两个方面有关系:

  • 进程的虚拟内存空间上限:创建以个线程,操作系统就需要为其分配一个栈空间,如果线程数量越多,所需要的栈空间就越大,那么虚拟内存就会占用的越多。
  • 系统参数限制 :虽然Linux没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程数

32位

假设创建一个线程需要占用10M的虚拟内内存,总共有3G虚拟内存可以使用,于是差不多可以创建300个左右的线程。如果想创建上千个线程,可以调整**创建线程时分配的栈空间大小比如调整成512K。ulimit -s 512

64位

64位系统如果按照上面的方法来计算可以创建的线程数量,会发现可以创建一千多万个线程,这明显是不可能的,因为还有系统的限制。(还会有CPU瓶颈问题)
下面这三个参数都会影响线程创建的上限:

/proc/sys/kernel/threads-max,表示系统支持的最大线程数,默认值是 14553;
/proc/sys/kernel/pid_max,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 32768;
/proc/sys/vm/max_map_count,表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是 65530。

多进程和多线程

多进程

每一个进程是资源分配的基本单位。

  • 进程结构由代码段、堆栈段、数据段组成。代码段是静态的二进制代码,多个程序可以共享。

父子进程

父进程创建子进程之后,除了pid之外几乎全部都一样。共享全部数据,但是子进程在读写数据的时候会通过写时复制机制将公共数据重新拷贝一份,然后再拷贝的数据上进行操作
如果子进程想要运行自己的代码段,可以通过调用execv()函数重新加载新的代码段,然后就和父进程独立开了。(在shell中执行程序就是通过shell进程先fork一个子进程然后通过execv()重新加载新的代码段的过程)

进程创建方式

整个Linux系统的所有进程也是一个树形结构,树根是由系统自动构造的,即在内核态执行的0号进程,它是所有进程的祖先。

第一种方式

参考Linux中的0号进程和1号进程
由0号进程创建1号进程(内核态),1号负责执行内核部分初始化工作以及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程,随后1号调用execve()运行可执行程序init,并演变成用户态1号(init进程)。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号的若干终端注册进程Getty。每个Getty进程设置其进程标识号,并监视配置到系统终端 的接口线路,当检测到来自终端的连接信号时,getty进程通过函数evecve()执行注册程序login,此时用户就可以输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell进程直接或者间接产生其他进程。

1号内核进程调用执行init并且演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。
init()函数在内核态运行,是内核代码;init()进程是内核启动并运行的第一个用户进程,运行在用户态下
1号进程调用execve()从文件/etc/inittab中加载可执行程序init()并执行,这个过程没有调用do_fork(),因此两个都是1号进程

第二种方式

父进程创建

进程退出

正常退出
  • exit()和__exit(),都会进行终止进程并做收尾工作,但是后者关闭全部描述符和清理函数之后不会刷新流,但是前者会在调用__exit()之前刷新数据流
  • return:exit()是函数,有参数,执行完之后控制权交给系统。return若是在调用函数中,执行完之后控制权交给调用进程,若是在main函数中,控制权交给系统
异常退出

abort(),终止信号

多线程

同一个进程内部有多个线程,所有线程共享同一个进程的内存空间,进程中定义的全局变量会被所有的线程共享,多个线程被CPU调度的顺序是不可控的所以对临界资源的访问要注意安全。

做一次简单的i=i+1在计算机中不是原子操作,涉及内存取数、计算和写入内存几个环节。而线程切换有可能发生在上述任何一个环节中间,所以不同操作顺序有可能带来意想不到的结果。

多线程的优点

会使原先顺序执行的程序被拆分成几个独立的逻辑流,可以独立完成一些任务

使用多线程应该注意的问题

  • 线程是否有先后访问顺序
  • 多个线程共享访问同一个变量(同步互斥问题)

每个线程是有自己独立的栈空间的,线程彼此之间是无法访问其他线程栈上内容的

Linux进程控制

虚拟地址空间

虚拟存储器为每个进程提供了独占系统地址空间的假象。好像每个进程都在独占地使用主存。每个进程看到的存储器都是一致的,称之为虛拟地址空间。

有一些敏感的地址:对于32位进程来说,代码段从0X08048000开始,从0XC0000000开始到0XFFFFFFFF是内核地址空间,通常情况下代码运行在用户态(0X00000000~0XC0000000的用户地址空间),当发生系统调用、进程切换等操作时CPU控制寄存器设置模式位,进入内核模式,这个状态下进程可以访问全部存储器位置和执行全部指令。(32位进程地址空间都是4G,但是用户态下只能访问低3G的地址空间,若要访问3G到4G的地址空间则要进入内核态)

PCB

进程调度实际上就是内核选择响应的进程控制块,被选择的进程控制块中包含了一个进程基本信息(进程标识符、处理及状态、进程调度信息、进程控制信息)

上下文切换

内核管理所有进程控制块,而进程控制块记录了进程全部状态信息,每一次进程调度就是一次上下文切换;上下文本质就是当前运行状态,主要包括**通用寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈和内核数据结构(页表、进程表、文件表)等。

一次完整的上下文切换通常是进程原先运行于用户态,之后因为系统调用或者时间片切换到内核态执行内核指令,完成上下文切换之后回到用户态,此时已经切换到了另一个进程。

进程调度算法

  • 先来先服务:非抢占式,不利于短作业
  • 短作业优先:非抢占式,长作业有可能饿死
  • 最短剩余时间优先:抢占式。按照剩余运行时间,如果新进程需要的时间更少,则挂起当前进程。
  • 时间片轮转:时间片用完的时候,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列末尾,同时CPU时间分配给队首的进程。时间片太小会导致进程切换频繁,时间片过长,实时性不能得到保证。
  • 优先级调度:为每个进程分配一个优先级,为了防止优先级低的任务等待不到调度,可以随着时间的推移增加等待进程的优先级
合理地设置各类进程的优先级:

参考操作系统——调度算法
①系统进程优先级高于用户进程;
②前台进程优先级高于后台进程;
③操作系统更偏好 I/O 型进程(I/O 繁忙型进程)。

  • 与 I/O 型进程相对的是计算型进程(CPU繁忙型进程),I/O 设备和 CPU 可以并行地工作,所以如果让 I/O 型进程优先运行的话,则越有可能让 I/O 设备尽早地投入工作,则资源的利用率和系统吞吐量等都会得到提升。

根据优先级是否可以动态地改变,可以将优先级分为静态优先级动态优先级两种。静态优先级在创建进程时就已经确定,之后一直不变;动态优先级创建进程时有一个初始值,之后会根据情况动态地调整优先级。
动态优先级的调整时机:从追求公平、提升资源利用率等角度考虑。如果某进程在就绪队列中等待了很长时间,可以适当提升其优先级;如果某进程占用处理机运行了很长时间,可以适当降低其优先级;如果发现一个进程频繁地进行I/O操作,可以适当提升其优先级。
优缺点:优点是用优先级区分紧急程度、重要程度,适用于实时操作系统,可灵活地调整对各种作业或进程的偏好程度;缺点是若源源不断地有高优先级进程到来,则可能导致饥饿。

多级反馈队列

对于需要连续执行多个时间片的进程,设置了多个队列,每个队列时间片大小不同比如1,2,4,8……。可以看作是时间片轮转调度算法和优先级调度算法结合。

进程通信

进程是一个独立的资源分配单元。不同进程(这里通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立的,不同的进程需要进行信息的交互和状态传递,因此需要进程通信。

进程通信的目的包括数据传输、通知事件、资源共享、进程控制
图片来源牛客网高性能并发服务器
在这里插入图片描述

图片来源阿秀的学习笔记

在这里插入图片描述

管道

无名管道(内存文件)

管道是一种半双工通信方式(只能由一端向另一端发送数据),数据只能单向流动,而且只能在具有亲缘关系的进程中使用。亲缘关系通常是指父子进程关系,指的是具有公共祖先的进程。

  • 管道其实是一个在内核内存中维护的缓冲器。是一种特殊的文件
  • 管道拥有文件的特质:读操作、写操作
  • 匿名管道没有文件实体,有名管道具有文件实体,单数不存储数据。可以按照操作文件的方式对管道进行操作
  • 一个管道是一个字节流,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块 的大小是多少。
    -管道的逻辑结构是一个先进先出的队列,逻辑结构是一个环形缓冲区
有名管道(FIFO文件,借助文件系统)

也是半双工通信方式,但是允许在没有亲缘关系的进程之间使用,先进先出的通信方式。

提供了一个路径名与管道关联,以FIFO的文件形式存在文件系统中,作为一个特殊文件存在,但是FIFO中的内容存放在内存中。

  • 创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

一旦使用mkfifo创建了一个FIFO,就可以使用open打开它。命名管道通过命令mkfifo或系统调用mkfifo来创建

共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但是多个进程都可以访问。共享内存是IPC里面最快速的方式(不同进程间使用事实上的同一块区域,使得进程间使用信息的时候免去“复制”这一流程,减少开销。

共享内存通常与信号量和管道结合使用,实现更加复杂的进程间通信
操作系统提供了一些共享内存的系统调用,例如shmget(获取共享内存区域)、shmat(映射共享内存区域)、shmdt(解除映射共享内存区域)和shmctl(共享内存控制操作)。
共享内存存在一些风险,因为多个进程都可以访问同一块内存区域,所以需要通过互斥机制来确保数据同步和安全
使用共享内存的时候要注意内存大小限制、内存清理和维护等

参考阿秀的学习笔记

相关接口

  • 创建共享内存:int shmget(key_t key, int size, int flag);
    成功时返回一个和key相关的共享内存标识符,失败返回-1。
    ·key:为共享内存段命名,多个共享同一片内存的进程使用同一个key。
    ·size:共享内存容量。
    ·flag:权限标志位,和open的mode参数一样。
  • 连接到共享内存地址空间:void *shmat(int shmid, void *addr, int flag);
    返回值即共享内存实际地址。
    ·shmid:shmget()返回的标识。
    ·addr:决定以什么方式连接地址。
    ·flag:访问模式。
  • 从共享内存分离:int shmdt(const void *shmaddr);
    调用成功返回0,失败返回-1。
    shmaddr:是shmat()返回的地址指针。

消息队列

消息队列是消息的连接表,包括POSIX消息对和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息;

  • 消息队列是一种被用于进程间通信的有序队列,将消息以FIFO顺序传递给接收进程。

操作系统提供了一些消息队列的系统调用,例如msgget(创建或打开一个队列)、msgsnd(向队列中发送一个消息)、msgrcv(从队列中接收一个消息)和msgctl(消息队列控制操作)等。

  • 比起管道,消息队列有这些优点:
    1、大量数据传输。因为消息队列可以通过多次发送和接收操作传输一个很大的数据块
    2、有格式数据:和管道不同,消息队列可以传输有个事的数据。==因为消息队列传输的式消息,可以根据需要将消息格式化成一定的形式,使数据更加灵活方便
    3、没有缓冲区大小限制,可以传递任意大小的数据块,不用担心缓冲区溢出
    4、和管道不同,消息队列可以支持多个进程同时并发访问。
    5、消息队列可以存储未读信息,这意味着即使接收进程不可用,发送进程任然可以将消息发送到队列中等待接收。
    6、消息队列可以独立于读写进程存在,从而避免了FIFO中同步管道的打开和关闭可能产生的困难
    7、读进程可以根据消息类型有选择的接收消息,而FIFO是默认接收的。

  • 消息队列是一种可靠的进程间通信机制,允许接收进程在消息到达前等待消息到达,并为进程提供了一种可靠的同步机制。

消息队列为进程提供了以下可靠机制:

  • 独立性:进程可以独立地执行,并且可以将消息发送给其他进程,而不必考虑接收进程何时接收消息。
  • 可扩展性:消息队列允许多个进程共享一个队列,并在需要时向队列中添加或删除操作。
  • 缓冲机制:消息队列为进程提供了一种可靠的缓冲机制,可以在繁忙的进程间传递消息,从而避免了消息丢失的可能性。
  • 可靠性:消息队列提供了一种可靠的进程间通信机制,允许接收进程在消息到达前等待消息的到达。

信号

用于通知进程某个事件已经发生。按crtl+c就是一种信号

除了用于进程间通信之外,进程还可以发送信号给进程本身:

  • 强制进程重置自身状态,发送SIGTERM信号终止当前操作并重置自身状态
    强制进程退出:SIGKILL强制终止自身
    程序执行控制:进程可以使用SIGUSR1和SIGUSR2等自定义信号来实现自身的程序控制执行

信号量

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,实现进程、线程对临界区的同步以及互斥访问。多进程使用的是SYSTEM V信号量。

信号量主要作为进程间或者同一进程不同线程的同步手段

内存映射

操作系统将一个文件映射到进程的地址空间中,以便直接访问文件内容。内存映射将文件的一部分或者全部内容映射到进程的地址空间的一段虚拟内存中,进程可以像访问普通内存一样访问这些数据。

内存映射是一种虚拟内存管理技术,它可以将磁盘文件映射到进程的地址空间中。
内存映射提高了IO操作的效率,因为读取和写入文件的内容可以像访问内存一样快速完成,此外,还可以提高进程之间的通信效率,因为多个进程可以访问映射同一文件的不同部分。
内存映射还提供了一种更加灵活的IO操作方式,例如将文件映射到不同进程或者不同地址空间中。

内存映射和共享内存的关系?区别是什么?

首先,内存映射和共享内存都是用于进程之间通信和共享数据的机制。它们主要作用是提高进程间通信的效率和简化程序实现。

  • 区别:
    1、对象:内存映射将一个文件映射到进程地址空间;共享内存是将一块内存在多个进程之间共享;
    2、访问方式:mmap通过虚拟内存机制实现磁盘数据直接映射到进程内存中,支持随机访问;共享内存需要使用系统显式操作内存,支持随机访问
    3、同步方式:mmap通常需要使用信号量等同步机制确保进程访问文件的正确性;共享内存可以通过锁等机制来确保多个进程对共享数据的顺序访问。
  • 相似
    1、都是用于进程通信和共享数据
    2、都支持高效率数据共享,可以显著提高程序执行效率
    3、都存在同步机制,确保数据完整性和安全性

socket

可以用于不同机器之间的通信(网络通信)

辅助命令

ipcs命令用于报告共享内存、信号量和消息队列信息。

  • ipcs -a:列出共享内存、信号量和消息队列信息。
  • ipcs -l:列出系统限额。
  • ipcs -u:列出当前使用情况。

线程通信

图片来源阿秀的学习笔记
在这里插入图片描述

Linux

信号

类似于进程间的信号处理

锁机制

1、互斥锁
2、自旋锁
3、读写锁

读写锁是一种在多线程编程中的同步机制,用于控制同一时刻由多少个读操作和写操作。
读写锁由两种锁定状态:读锁和写锁。读锁状态下,多个线程可以同时读取共享资源,但是只有一个线程可以获取写锁来修改这个资源。写锁状态下,只有一个线程可以修改共享资源,其它线程无论读或者写都会被阻塞,直到写锁被释放。读写锁的目的是使多个线程能够同时读取共享资源,提高并发性能。

读写锁有两种实现方式,读优先和写优先。读优先使可以使得多个线程获得读锁,但是当一个线程有写锁的时候,其它线程都被阻塞;写优先是优先处理写操作,写操作处理完之后才处理读操作。

读写锁通常被用于对于读操作频繁但是写操作较少的共享资源,例如数据库查询。可以提高并发性能,减小锁的争用,降低线程之间的竞争。

条件变量

使用通知的方式解锁,与互斥配合使用

信号量

包括无名线程信号量和命名线程信号量。多线程同步的信号量是POSIX信号量。

Windows

  • 全局变量:需要有多个线程来访问一个全局变量时,通常我们会在这个全局变量前加上volatile声明,以防编译器对此变量进行优化
  • Message消息机制:常用的Message通信的接口主要有两个:PostMessage和PostThreadMessage,PostMessage为线程向主窗口发送消息。而PostThreadMessage是任意两个线程之间的通信接口。
  • Message消息机制:常用的Message通信的接口主要有两个:PostMessage和PostThreadMessage,PostMessage为线程向主窗口发送消息。而PostThreadMessage是任意两个线程之间的通信接口。

猜你喜欢

转载自blog.csdn.net/qaaaaaaz/article/details/130375402