操作系统相关知识整理(二)

进程和线程

进程与线程的区别

1、进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

进程空间

Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:
- 程序段 (Text Segment):可执行文件代码的内存映射
- 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
- BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
- 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
- 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
- 映射段(Memory Mapping Segment):任何内存映射文件

线程空间

线程之间共享进程空间,在内存映射区域开辟一块独有的线程栈。

线程共享的环境包括: 进程代码段、进程的公有数据( 利用这些共享的数据,线程很容易的实现相互之间的通讯) 、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户 ID 与进程组 ID 。
线程之间特有的:每个线程都有自己独立的线程上下文,包括线程 ID 、栈、栈指针、程序计数器、条件码和通用目的寄存器值。
线程之间共有的:共享进程上下文的剩余部分,包括只读文本(代码)、读/ 写数据、堆以及所有的共享库代码和数据区域。线程也共享相同的打开文件的集合。

进程的创建

在 在 linux 中主要提供了 fork ,vfork ,clone 三个进程创建方法。
fork:现在 Linux 中是采取了 copy-on-write(COW 写时复制) 技术,为了降低开销,fork 最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着 parent 和 child 的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。


vfork():vfork 系统调用不同于 fork,用 用 vfork 创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。但此处有一点要注意的是用 vfork()创建的子进程必须显示调用 exit()来结束,否则子进程将不能结束,而 fork()则不存在这个情况。


用 vfork 创建子进程后,父进程会被阻塞直到子进程调用 exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork 的好处是在子进程被创建后往往仅仅是为了调用 exec 执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过 vfork 共享内存可以减少不必要的开销。

clone():系统调用 fork()和 vfork()是无参数的,而 clone()则带有参数。fork()是全部复制,vfork()是共享内存,而 clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。

Unix环境高级编程中8.3节中说,“子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。”

线程的创建

实际上是调用了进程创建的clone()方法,对进程描述符task_struct中的内存描述符mm_struct作浅拷贝,再调用mmap在内存映射区域创建线程栈。

进程状态

R (TASK_RUNNING),可执行状态。

S (TASK_INTERRUPTIBLE),可中断的睡眠状态。

D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。

TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。

T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。

Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。

在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。

X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁。

线程状态

进程调度

参考这篇博客https://www.cnblogs.com/newjiang/p/7479965.html

进程间通信

IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket 、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC

管道

int pipe(int fd[2]); // 返回值:若成功返回 0,失败返回-1,半双工

有名管道

FIFO 可以在无关的进程之间交换数据,与无名管道不同。

int mkfifo(const char *pathname, mode_t mode);

消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID )来标识。
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量

信号量(semaphore )与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。

共享内存

共享内存(Shared Memory ),指两个或多个进程共享一个给定的存储区。
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

各种IPC的优缺点

五种通讯方式总结
管 道:速度慢,容量有限,只有父子进程能通讯
FIFO :任何进程间都能通讯,但速度慢
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
信号量:不能传递复杂消息,只能用来同步

共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时
候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

线程同步

条件变量为什么要搭配互斥锁
1 条件变量的改变一般是临界资源来完成的,  那么修改临界资源首先应该加锁, 而线程在条件不满足的情况下要阻塞,等待别人唤醒, 那么在阻塞后一定要把锁放开,等到合适的线程拿到锁去修改临界资源,否则会出现死锁
2 在线程被唤醒后第一件事也应该是争取拿到锁, 恢复以前加锁的状态。 否则在执行条件变量成立后的代码也法保证其原子性。
所以条件变量和锁是相辅相成的:条件变量需要锁的保护锁需要条件变量成立后,后重新上锁。 

内存池,进程池,线程池

内存池是池化技术中的一种形式。通常我们在编写程序的时候回使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次申请内存和释放内存的时候,都需要和操作系统的系统调用打交道,从堆中分配所需的内存。如果这样的操作太过频繁,就会找成大量的内存碎片进而降低内存的分配性能,甚至出现内存分配失败的情况。


而内存池就是为了解决这个问题而产生的一种技术。从内存分配的概念上看,内存申请无非就是向内存分配方索要一个指针, 当向操作系统申请内存时,操作系统需要进行复杂的内存管理调度之后,才能正确的分配出一个相应的指针。而这个分配的过程中,我们还面临着分配失败的风险。


所以,每一次进行内存分配,就会消耗一次分配内存的时间,设这个时间为 T,那么进行 n 次分配总共消耗的时间就是 nT;如果我们一开始就确定好我们可能需要多少内存,那么 在最初的时候就分配好这样的一块内存区域, 当我们需要内存的时候,直接从这块已经分配好的内存中使用即可,那么总共需要的分配时间仅仅只有 T。当 n 越大时,节约的时间就越多。

线程池 是一种多线程处理形式,将任务添加到队列中,然后在创建线程后,  自动启动这些任务。 线程池线程都是后台线程。
使用线程池理由:1. 频繁创建、 销毁加大系统开销,影响处理效率。2. 限制线程最大并发数。3. 管理线程

孤儿进程,僵尸进程,守护进程

父进程在调用 fork 接口之后和子进程独立开,之后子进程和父进程就以未知的顺序向下执行(异步过程)。所以父进程和子进程都有可能先执行完。 当父进程先结束,子进程此时就会变成孤儿进程 ,被 不过这种情况问题不大,孤儿进程会自动向上被 init 进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素。

如果子进程先结束,父进程并未调用 wait 或者 waitpid 获取进程状态信息,那么子进程描述符就会一直保存在系统里,这种进程称为僵尸进程。

守护进程:
定义:守护进程是脱离终端并在后台运行的进程,执行过程中信息不会显示在终端上并且也不会被终端发出的信号打断。

自旋锁

自旋锁是一种用于保护多线程共享资源的锁,与一般的互斥锁(mutex)不同之处在于当自旋锁尝试获取锁的所有权时会以忙等待(busy waiting)的形式不断的循环检查锁是否可用。

在多处理器环境中对 持有锁时间 较短的程序来说使用自旋锁代替一般的互斥锁往往能提高程序的性能。
自旋锁有两种基本状态:
1. 锁定状态
锁定状态又称不可用状态,当自旋锁被某个线程持有时就是锁定状态,在自旋锁被释放之前其他线程不能获得锁的所有权。
2. 可用状态
当自选锁未被任何线程持有时的状态就是可用状态。假设某自旋锁内部使用 bool 类型的 flag 变量来标识自旋锁的状态。当 flag 为 true 表示锁定状态,为 false 表示可用状态。

协程

协程,英文 Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度,最重要的是,协程不是被
操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程暂停和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。因此,协程的开销远远小于线程的开销。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值