详解Linux下多进程与多线程通信(一)

一.多进程与多线程

多进程和多线程都是多任务处理的方法,它们允许计算机同时执行多个任务。它们在资源分配、通信机制、内存管理等方面有着根本性的区别。

1.多进程

多进程指的是操作系统能够同时管理和执行多个进程,每个进程有自己独立的内存空间。这意味着进程之间的通信需要特别的机制,如管道、信号量、共享内存或消息队列等。

多进程特点:

a.稳定性高:一个进程崩溃通常不会影响其他进程,因为它们的内存空间是隔离的。

b.安全性:由于内存是隔离的,所以进程之间的数据不易被未授权访问。

c.资源消耗大:每个进程都有自己的内存和系统资源,这可能会导致更高的内存使用和较慢的切换时间。

d.开发和维护可能更复杂:进程间通信比线程间通信更复杂。

2.多线程

多线程是在单一进程内部创建多个线程,这些线程共享进程的内存空间和资源,但每个线程都有自己的执行序列。

多线程特点:

a.资源消耗小:线程之间共享内存和资源,创建和上下文切换的开销较小。

b.响应速度快:线程可以很快地进行交互和通信,因为它们共享相同的内存空间。

c.稳定性问题:一个线程崩溃可能会影响整个进程,因为所有线程共享相同的地址空间。

d.安全性问题:需要确保线程安全,避免数据冲突和不一致性。

3.多进程与多线程的选择

选择使用多进程还是多线程通常依赖于应用程序的需求和特性。如果需要高度的稳定性和隔离性,多进程可能是更好的选择。如果任务之间需要频繁交互,并且对资源使用有严格的要求,多线程可能更合适。在现代操作系统中,有时会同时使用多进程和多线程。这种方式结合了多进程的稳定性和多线程的高效性。

二.进程

1.进程的内存布局

程序一直都是由以下几部分组成的:

**正文段:**也称为代码段,这是 CPU 执行的机器语言指令部分,文本段具有只读属性,以防止程序由于意外而修改其指令;正文段是可以共享的,即使在多个进程间也可同时运行同一段程序。

**初始化数据段:**通常将此段称为数据段,包含了显式初始化的全局变量和静态变量,当程序加载到内存中时,从可执行文件中读取这些变量的值。

**未初始化数据段:**包含了未进行显式初始化的全局变量和静态变量,通常将此段称为 bss 段,在程序开始执行之前,系统会将本段内所有内存初始化为 0,可执行文件并没有为 bss 段变量分配存储空间,在可执行文件中只需记录 bss 段的位置及其所需大小,直到程序运行时,由加载器来分配这一段内存空间。

**栈:**函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,每次调用函数时,函数传递的实参以及函数返回值等也都存放在栈中。栈是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。

**堆:**可在运行时动态进行内存分配的一块区域,譬如使用 malloc()分配的内存空间,就是从系统堆内存中申请分配的。

典型的布局如下图所示:

2.进程的虚拟地址空间

在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间。

当所有的应用程序访问的内存地址就是实际的物理地址,当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。 内存空间不足时,就需要将其它程序暂时拷贝到硬盘中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率就会非常低。由于程序是直接访问物理内的,所以每一个进程都可以修改其它进程的内存数据,甚至修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏,系统不安全、不稳定。程序运行时,链接地址和运行地址必须一致,否则程序无法运行!因为

程序代码加载到内存的地址是由系统随机分配的,是无法预知的,所以程序的运行地址在编译程序

时是无法确认的。

引入虚拟地址后,有一下优点:

1.进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。

2.在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。

3.便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施,例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。

4.编译应用程序时,无需关心链接地址。前面提到了,当程序运行时,要求链接地址与运行地址一致,在引入了虚拟地址机制后,便无需关心这个问题。

三.线程

1.线程的概念

线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。

当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。

任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create 创建一个新的线程),那么创建的新线程就是主线程的子线程。

线程是程序最基本的运行单位,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。

四.多线程问题

1.互斥与同步问题

互斥问题指的是多个线程对共享资源的访问冲突,通常表现为多个线程试图同时读写同一份数据,导致数据不一致或者不可预测的行为。为了避免这种情况,需要通过互斥机制确保同一时刻只有一个线程能够访问共享资源。

同步问题指的是线程间的协作问题,例如一个线程需要等待另一个线程完成某个任务,或者需要确保线程按照特定顺序执行。同步通常涉及线程之间的协调,确保它们在正确的时机访问共享资源或按顺序执行。

2.互斥锁

互斥锁是一种用于保证某段代码在任何时刻只能由一个线程执行的同步机制。当一个线程获得互斥锁后,其他线程必须等待直到该线程释放锁,才能获得访问权限。

3.条件变量

条件变量通常与互斥锁一起使用,允许线程在某些条件还未达成时挂起,直到另一个线程改变了条件并通知该条件变量。在pthreads库中使用pthread_cond_t实现。

4.读写锁

读写锁允许多个线程同时读共享数据,但如果一个线程要写数据,则需要独占访问。这是一个适用于读多写少情形的同步机制。在pthreads中,它们通过pthread_rwlock_t实现。

5.自旋锁

自旋锁是一种在等待释放锁的时候持续检查锁的状态而不是进入睡眠状态的锁。它们适用于锁只会被持有很短时间的情况,因为它们避免了线程睡眠和唤醒所需的系统调用开销。在Linux中,可以使用pthread_spinlock_t或者原子操作实现自旋锁。

6.信号量

信号量是一种较为底层的同步机制,可以用来实现互斥锁和条件变量以及其他同步模式。信号量使用计数器来控制对共享资源的访问,并可以用来实现线程间的同步。在Linux中,信号量可以通过POSIX信号量(sem_t)或System V信号量实现。

7.同样可以实现互斥,互斥锁和信号量的区别

互斥锁(Mutexes)和信号量(Semaphores)都可以用于实现线程或进程间的互斥,即确保在同一时间只有一个线程或者进程可以访问一个共享资源。尽管它们的目标相同,但它们在概念上和使用方式上存在一些关键的差异:

a.基本概念:

互斥锁:设计为防止多个线程同时访问共享资源。互斥锁在任何时刻只能被一个线程持有。如果一个线程已经持有互斥锁,其他尝试获取该互斥锁的线程将被阻塞,直到锁被释放。

信号量:是一种更为通用的同步机制,它包含一个计数器,用来控制多个线程对共享资源的访问。信号量可以允许多个线程同时访问共享资源,计数器的值代表了可以同时访问该资源的线程数目。

b.用途和适用性

互斥锁:专门用于保证互斥,即一次只有一个线程访问某资源。因此,它们通常用于保护对共享资源的访问,避免数据竞争。

信号量:可以用于多种同步问题,包括互斥(计数器设置为1的信号量)、限制对资源的并发访问数目、信号传递(例如,用作两个线程之间的信号)等。

c.所有权

互斥锁:有所有权的概念,即只有锁定互斥锁的线程才能够释放它。如果其他线程试图释放一个不属于它的互斥锁,通常会导致错误。

信号量:没有所有权的概念,任何一个线程都可以增加或减少计数器,独立于其他线程的操作。

d.复杂性

互斥锁:通常比信号量简单,因为它们只在两个状态之间切换:锁定和未锁定。

信号量:可以更复杂,因为它们可以在多个状态之间切换,取决于信号量的计数器值。

五.进程间的通信

1.管道

普通管道 pipe:通常有两种限制,一是单工,数据只能单向传输;二是只能在父子或者兄弟进程间使用;

流管道 s_pipe:去除了普通管道的第一种限制,为半双工,可以双向传输;只能在父子或兄弟进程间使用;

有名管道 name_pipe(FIFO):去除了普通管道的第二种限制,并且允许在不相关(不是父子或兄弟关系)的进程间进行通讯。

2.信号

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

3.消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和 System V 消息队列。

消息队列是 UNIX 下不同进程之间实现共享资源的一种机制, UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。

4.信号量

信号量是一个计数器,与其它进程间通信方式不大相同,它主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。

它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。Linux 提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件 sys/sem.h 中。

5.共享内存

共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的,它往往与其它通信机制,譬如结合信号量来使用,以实现进程间的同步和通信。

6.套接字

Socket 是一种 IPC 方法,是基于网络的 IPC 方法,允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据,说白了就是网络通信。

在一个典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:

a.各个应用程序创建一个 socket。socket 是一个允许通信的"设备",两个应用程序都需要用到它。

b.服务器将自己的 socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。

相关推荐
小坏坏的大世界10 分钟前
ROS2中的QoS(Quality of Service)详解
linux·机器人
Jess071 小时前
归并排序递归法和非递归法的简单简单介绍
c语言·算法·排序算法
Ronin3051 小时前
【Linux系统】进程状态 | 进程优先级
linux·运维·服务器·ubuntu
易知嵌入式小菜鸡1 小时前
CCS-MSPM0G3507-7-模块篇-MPU6050的基本使用
linux·运维·服务器
浅水鲤鱼2 小时前
欧拉系统安装UKUI桌面环境
linux·运维·服务器
TEC_INO2 小时前
Linux_3:进程间通信
linux·运维·服务器
Insist7532 小时前
linux系统---部署应用
linux·运维·服务器
<但凡.2 小时前
Linux修炼:开发工具
linux·服务器·bash
双叶8362 小时前
(C++)STL标准库(vector动态数组)(list列表)(set集合)(map键值对)相关对比,基础教程
c语言·开发语言·数据结构·c++·list
是阿建吖!2 小时前
【Linux | 网络】应用层
linux·网络·php