操作系统 进程相关

发布时间:2024年01月11日

1 进程、线程、协程

定义

【Are u OKay?——协程、线程、进程】 https://www.bilibili.com/video/BV1Wr4y1A7DS/?share_source=copy_web&vd_source=1e4d767755c593476743c8e4f64e18db

高并发:线程池,不要无休止的创建线程。--> task很小很快,thread需要切换(中断)-->协程

拥有资源

切换过程、开销

进程切换:

  1. 上下文保存(保存当前进程状态): 当操作系统决定切换到另一个进程时,首先需要保存当前进程的上下文信息,包括寄存器的状态、程序计数器、内存页表等。

  2. 切换到内核模式: 进程切换通常涉及从用户模式切换到内核模式,以便操作系统能够执行敏感的指令并更改进程的状态。

  3. 调度新进程: 操作系统选择要切换到的新进程,将其上下文信息加载到 CPU 寄存器和内存管理单元中。

  4. 切换到用户模式: 切换到新进程的用户模式,允许其执行。

在任务切换的过程中,通常会涉及到系统调用。系统调用是用户程序与操作系统之间进行通信的一种方式,用于请求操作系统提供服务。在任务切换中,以下情况可能触发系统调用:

  1. 上下文保存: 将当前进程的上下文信息保存到内存中,可能涉及到对内存的写操作,这可能需要调用操作系统提供的服务。

  2. 切换到内核模式: 用户程序切换到内核模式通常需要通过中断或异常来触发,这会涉及到一些与特权级别相关的系统调用,例如进入内核模式的中断服务例程。

  3. 调度新进程: 选择和调度新进程也可能涉及到系统调用,例如获取进程列表、更新进程状态等。

通信、并发

进程是应用程序/程序的执行副本,进程要管理硬件资源、CPU资源、文件资源、内存分页等,执行只需要CPU和内存。进程来回切换消耗资源,所以抽象出一个更小的单位线程,当一个程序启动以后(进程),会产生一个主线程,操作系统将计算资源不给进程,而是直接给主线程。进程是资源管理的单位,线程是程序执行的单位,线程只需要执行程序。操作系统的调度线程的执行,一个一个线程排队执行,每个线程执行时都有一个时间片,当时间片执行完了以后,切换下一个线程执行。程序执行产生一个进程,这个进程都会有一个主线程。但是操作系统调度的是自己的线程,自己的线程表,程序员在用户空间创建线程,但是真正执行的是操作系统的线程。操作系统的线程和用户线程是映射关系,也就是说,操作系统的线程才是真正的线程,程序员并不能直接执行直接的线程,必须要挂靠到操作系统的线程执行。高并发的场景下,会有很多task,为每个task创建一个线程(线程对象)很占用内存空间,每个线程都对应一个操作系统的线程(内核线程),这样操作系统忙不过来。所以设计了线程池技术,溢出线程池怎么办,让task排队(线程会回收到线程池)。

2 Linux 进程通信方式

管道

消息队列

  1. 内核中的链表: 消息队列通常在内核中维护一个链表,用于存储进程发送的消息。每个消息都是链表中的一个节点,包含了消息内容、发送者、接收者等信息。这样的设计使得内核能够高效地管理和调度消息。

  2. 系统调用: 在Linux系统中,消息队列的相关操作是通过系统调用来实现的。

共享内存

基于虚拟内存实现,不需要走系统调用。往往配合信号量

信号量(同步,限制临界资源)

pv操作

套接字

信号

3 线程通信

信号

锁机制

条件变量

信号量

4 Linux 同步

  • POSIX信号量:可用于进程同步,也可用于线程同步。

  • POSIX互斥锁 + 条件变量:只能用于线程同步。

5 进程同步

  1. 互斥锁(Mutex):

    • 使用互斥锁来确保一次只有一个进程能够进入临界区(一段关键代码)。
    • 进入临界区前先尝试获得锁,如果锁已经被其他进程占用,则等待。
  2. 信号量(Semaphore):

    • 信号量是一种更为通用的同步工具,可以用来实现互斥和合作。
    • 可以用于控制对共享资源的访问,也可以用于进程之间的通信。

读者写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define N 100
typedef int semaphore;

semaphore mutex = 1;    // 互斥信号量,用于对临界区的访问进行互斥
semaphore empty = N;    // 表示缓冲区中的空槽位数量
semaphore full = 0;     // 表示缓冲区中的已有数据项数量

// 生产者函数
void producer() {
    while (1) {
        int item = produce_item();  // 生产一个数据项

        down(&empty);   // 等待缓冲区有空槽位可用
        down(&mutex);   // 进入临界区前先获取互斥锁

        insert_item(item);  // 将数据项插入缓冲区
        up(&mutex);         // 退出临界区,释放互斥锁
        up(&full);          // 增加已有数据项数量
    }
}

// 消费者函数
void consumer() {
    while (1) {
        down(&full);   // 等待缓冲区中有数据项可用
        down(&mutex);  // 进入临界区前先获取互斥锁

        int item = remove_item();  // 从缓冲区中移除数据项
        consume_item(item);        // 消费数据项

        up(&mutex);  // 退出临界区,释放互斥锁
        up(&empty);  // 增加空槽位数量
    }
}
  1. 条件变量(Condition Variable):

    • 条件变量用于在某个条件得到满足之前使线程等待,只有当条件满足时,才唤醒等待的线程。
    • 通常与互斥锁一起使用,以确保在检查条件和等待/唤醒之间的操作是原子的。
  2. 屏障(Barrier):

    • 用于协调多个线程在程序中的特定点上同步。当所有线程都到达屏障点时,它们才能继续执行。
  3. 读写锁(Read-Write Lock):

    • 读写锁允许多个线程同时读取共享资源,但在写操作时需要互斥。
    • 适用于读多写少的场景,提高了读取操作的并发性。
  4. 自旋锁(Spin Lock):

    • 自旋锁是一种忙等待的锁,线程在获取锁失败时会一直循环检查,直到获取到锁为止。
    • 适用于临界区很小、锁占用时间短的情况。

6 进程状态切换

就绪、运行、阻塞

7 进程和线程

启动、开销

一致性

线程执行顺序不确定

抢占式调度: 操作系统通常采用抢占式调度方式,即在任意时刻都可能中断当前执行的线程,切换到其他就绪状态的线程。这样的调度策略导致线程执行的时机和顺序不确定。

共享资源

  1. 堆(Heap): 堆是由程序员分配和释放的内存区域,用于存储动态分配的数据。多个线程可以访问同一块堆内存。

  2. 全局变量和静态变量: 全局变量和静态变量存储在程序的全局数据区,它们在程序的整个生命周期内存在。多个线程可以访问和修改这些变量,因此需要考虑线程安全性。

  3. 指针和引用: 指针和引用可以指向堆上的内存或者栈上的内存。如果它们指向的是共享的堆内存,多个线程就可以访问和修改这些数据。但如果指向的是栈上的内存,那么每个线程将有自己独立的拷贝。

  4. 文件: 文件描述符是全局唯一的,因此多个线程可以共享对同一文件的访问。

  5. 栈(Stack): 每个线程都有自己的栈,用于存储局部变量和函数调用信息。栈是线程私有的,每个线程都有独立的栈空间,不同线程之间不会共享栈。

总体来说,多线程之间可以共享堆、全局变量、静态变量、指针和引用所指向的内存,以及对文件的访问。而栈是每个线程私有的,不同线程之间不会共享栈。在多线程编程中,需要注意对共享资源的访问进行同步,以避免竞态条件和数据不一致的问题。

8 一个进程能创建多少个线程?

32/64

9 进程和线程模型

单进程多线程和多进程应用程序各自的优势?

单进程多线程:

  1. 资源共享: 线程共享同一进程的地址空间,可以方便地共享数据。线程之间的通信相对容易,无需复杂的 IPC 机制。--> 不同进程要系统调用
  2. 轻量级: 线程相比进程更轻量级,创建和销毁线程的开销较小。
  3. 并发性: 多线程可以在单个进程内实现并发,适合处理并行性较高的任务。

多进程:

  1. 隔离性: 进程拥有独立的地址空间,彼此隔离,一进程崩溃不影响其他进程。--> 多线程会影响
  2. 安全性: 进程之间的隔离性可以提高应用程序的安全性,降低错误传播的风险。
  3. 分布式计算: 多进程可以更容易地进行分布式计算,每个进程可以在不同的计算节点上执行。

选择依据:

  1. 任务性质: 如果应用程序的任务可以被有效地分解为并行执行的子任务,并且需要共享大量数据,那么多线程可能是一个更好的选择。如果任务需要完全独立执行,且不需要共享大量数据,多进程可能更合适。

  2. 可维护性: 多线程相对来说更容易维护,因为它们共享相同的地址空间,数据共享相对简单。但要注意线程同步和共享数据的问题。多进程在某种程度上更易于隔离和维护。

  3. 性能需求: 多线程可以提供更好的性能,特别是在多核处理器上。但多进程在某些情况下也能提供良好的性能,例如可以在多台机器上分布执行。

多进程

父子关系

进程控制(虚拟地址、上下文切换)

虚拟存储器是一种计算机系统的存储管理技术,为每个进程提供了一个独立、连续的虚拟地址空间,这个虚拟地址空间被映射到物理内存或者辅助存储器上。通过虚拟存储器,操作系统为每个进程提供了独占系统地址空间的假象,使得每个进程感觉自己在独占地使用整个系统的地址空间。

  1. 资源隔离和地址空间:

    • 进程上下文切换: 进程是独立的执行单位,各进程拥有独立的地址空间。因此,进程上下文切换会涉及到切换整个进程的地址空间,包括页表的切换。这是为了确保进程间的完全隔离,每个进程都有自己独立的地址空间。
    • 线程上下文切换: 线程是进程的轻量级执行单位,线程共享同一进程的地址空间。在线程上下文切换时,不涉及切换地址空间,因为线程共享相同的地址空间,页表保持不变。
  2. 切换成本:

    • 进程上下文切换: 由于进程上下文切换需要切换整个地址空间,包括页表,所以成本相对较高。页表的切换涉及到操作系统内核的介入,因此需要更多的操作。
    • 线程上下文切换: 线程上下文切换无需切换地址空间,只需保存和恢复线程私有的状态,因此成本相对较低。线程上下文切换更加轻量级。
  3. 并发性和资源共享:

    • 进程上下文切换: 进程提供了更大程度的独立性,但进程间通信相对复杂,需要使用进程间通信(IPC)机制。不同进程的数据不直接共享,需要通过显式的通信方式。
    • 线程上下文切换: 线程共享同一地址空间,相对容易进行数据共享,但需要注意同步和互斥。线程间的通信相对简单,可以通过共享内存等方式实现。

多线程

用户态多线程

依赖关系?资源访问?

10 进程调度

先来先服务

短作业优先

最短剩余时间优先

时间片轮转

多级反馈队列

11 守护 僵尸 孤儿

  1. 守护进程(Daemon Process)

    • 定义: 守护进程是在后台运行的一类特殊进程,通常独立于控制终端,并在系统启动时启动。
    • 特点: 它们通常用于执行系统级任务,不受特定用户登录或注销的影响。在后台默默运行,通常不与用户直接交互。
    • 应用: 例子包括网络服务(如Web服务器、数据库服务)、系统监控进程等。systemd是一种常见的守护进程管理器。
  2. 僵尸进程(Zombie Process)

    • 定义: 僵尸进程是已经结束执行(退出)的进程,但其父进程尚未调用wait()waitpid()来获取其终止状态信息的进程。
    • 原因: 父进程没有及时处理子进程的退出状态信息,导致子进程的资源仍然占用系统表项,但已经无法执行任何操作。
    • 应用: 父进程应该及时调用wait()waitpid()来处理子进程的终止状态,释放其资源。
  3. 孤儿进程(Orphan Process)

    • 定义: 孤儿进程是指其父进程已经终止或者不再关心它的状态的进程。
    • 原因: 父进程可能意外终止,或者在子进程执行期间父进程已经退出。
    • 应用: 孤儿进程会被init进程(通常是进程ID为1的进程)接管,并由init进程成为新的父进程。这确保孤儿进程在终止时能够正常退出。

在进程管理中,守护进程用于长期运行的系统服务,僵尸进程的管理涉及父进程对子进程状态的处理,孤儿进程的接管确保它们能够正常终止。这些概念帮助操作系统有效地管理和协调运行中的进程。

如何避免僵尸进程

僵尸进程有什么危害?内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。

1)子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程signal(SIGCHLD,SIG_IGN)通知内核表示自己对子进程的退出不感兴趣,那么子进程退出会立即释放数据结构。

2父进程通过wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将被阻塞待

3)如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()

12 中断和异常

在Linux中,异常和中断是两个不同的概念,但它们都涉及到处理由处理器生成的异步事件。下面是它们的区别和联系:

  1. 区别:

    • 异常(Exception): 异常是一种处理器内部的事件,通常表示一个错误或者不寻常的条件。例如,除以零、访问非法地址等都可以引发异常。异常通常是由当前运行的程序引发的。
    • 中断(Interrupt): 中断是一种来自处理器外部的事件,可以是硬件设备产生的信号,也可以是由其他处理器引发的信号。中断会打断当前正在执行的程序,转而执行与中断相关的处理程序。
  2. 联系:

    • 异常和中断的相似点: 它们都是处理器响应异步事件的机制,它们引发时会打断当前程序的正常执行流程。
    • 处理方式: 无论是异常还是中断,处理方式都是类似的,都需要操作系统介入。当异常或中断发生时,操作系统会根据相应的处理程序来处理这一事件。

总体来说,异常更侧重于处理器内部的错误或不寻常条件,而中断更侧重于来自处理器外部的事件,例如硬件设备的信号。在操作系统中,它们都需要相应的处理机制,以确保系统的正确运行和响应外部事件。

13?死锁

why & what?

多线程竞争共享资源可能导致数据错乱的原因主要有以下几点:

  1. 原子性问题: 多线程并发执行时,一个线程可能在读取共享数据的同时,另一个线程在修改这个数据,导致读取的数据不是一个完整的、一致的值。这种情况常称为原子性问题。

  2. 可见性问题: 多线程操作共享数据时,每个线程都有自己的本地缓存,当一个线程修改了共享数据,其他线程不一定能立即看到这个修改。这就是可见性问题,即一个线程对共享数据的修改对其他线程不可见。

  3. 有序性问题: 指令重排序可能导致线程执行的顺序与预期不符。编译器和处理器可能会对指令进行重排序以提高性能,但在多线程环境下,这可能导致共享数据的操作顺序不符合程序员的预期。

  4. 竞态条件: 当多个线程并发地访问共享资源,并且最终的结果取决于线程执行的时间顺序时,就可能发生竞态条件。竞态条件下,不同的线程得到的结果可能是不同的,从而导致数据错乱。

在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。

那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁

how?

如何排查死锁?

利用 top 命令查看进程的cpu占用率,然后看cpu占用率100%的进程的执行栈,然后等待的东西是同一个

排查死锁通常需要深入分析程序的执行状态和资源调度情况。以下是一些建议和步骤,供您在排查死锁时参考:

  1. 观察系统状态: 使用系统工具(如tophtopps等)观察系统的整体状态,查看CPU和内存的使用情况。死锁可能导致程序无法正常执行,使得某个进程的CPU占用率达到100%。

  2. 查看进程栈信息: 对于CPU占用率较高的进程,使用调试工具(如GDB)查看其执行栈信息。定位到占用CPU的线程,查看其执行状态和调用链。

  3. 在GDB中可以使用 bt 命令查看线程的回溯信息。

  4. 使用死锁检测工具: 一些编程语言和工具提供了死锁检测功能,例如Java中的jstack、C++中的线程检测工具等。这些工具可以帮助您定位死锁发生的位置。

  5. 分析日志和输出: 检查程序的日志输出,查找是否有关于死锁的异常信息。程序的输出可能包含关键的线程状态信息和死锁提示。

  6. 使用死锁预防策略: 在程序设计阶段,考虑使用死锁预防策略,例如避免使用多个锁、使用超时机制、使用事务等。

14 零拷贝是什么?有什么用?

收发网络数据属于高权限操作,和硬件相关,数据到内存,先到内核空间,然后从内核拷到用户空间。

做一个内存映射,优化这个过程。

零拷贝(Zero-Copy)是一种优化技术,主要用于减少数据在系统内部的复制次数,从而提高数据传输的效率。在传统的数据传输中,数据通常需要经过多次拷贝操作,从一个缓冲区拷贝到另一个缓冲区,涉及多次内存访问和复制操作,导致性能损耗。零拷贝的目标是通过一些技术手段来最小化或消除这些数据拷贝操作。

零拷贝的主要思想和用途:

  1. 内存映射: 使用内存映射技术,将文件映射到进程的地址空间,使得文件的内容可以直接在用户空间和内核空间之间共享,避免了数据在内核空间的中间复制。

  2. 文件传输优化: 在网络传输中,零拷贝可以通过直接操作内存映射的文件来避免数据从用户空间到内核空间的额外复制,提高文件传输效率。

15 几种典型的锁

读写锁

多个读者可以同时进行读-->不涉及对数据的更改

写者必须互斥(只允许一个写者写,也不能读者写者同时进行)--> 不能同时更改

写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责 线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以 ?互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线 ?程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个 ?线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足 ?时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变 ?量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互 ?斥的机制,条件变量则是同步机制。

import threading

mutex = threading.Lock()
condition = threading.Condition()
shared_resource = []

def producer():
    global shared_resource
    for i in range(5):
        with mutex:
            shared_resource.append(i)
            print(f"Produced: {i}")
        with condition:
            condition.notify()

def consumer():
    global shared_resource
    for i in range(5):
        with mutex:  # 获取互斥锁
            while not shared_resource:
                with condition:
                    condition.wait()  # 释放互斥锁并等待通知
            item = shared_resource.pop()
            print(f"Consumed: {item}")

producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

producer_thread.start()
consumer_thread.start()

producer_thread.join()
consumer_thread.join()

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。 ?如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的 ?场景,这个时候效率比较高。

自旋锁和互斥锁

  1. 互斥锁(Mutex Lock):

    • 互斥锁使用操作系统提供的底层系统调用(如 pthread_mutex_lockMutex 类)来实现。当一个线程尝试获得互斥锁时,如果锁已经被其他线程占用,那么它将被阻塞,直到锁被释放。
    • 互斥锁会导致线程被挂起,进入睡眠状态,从而释放 CPU 资源,等待锁变为可用。
    • 互斥锁适用于多线程环境下的长期等待和占用 CPU 时间较短的情况。

    优点:

    • 适用于线程等待时间较长的情况,不会浪费 CPU 时间。
    • 不会引发自旋带来的性能开销。

    缺点:

    • 在线程竞争激烈时,频繁的上下文切换和线程挂起会带来较高的性能开销。
  2. 自旋锁(Spin Lock):

    • 自旋锁是一种忙等待的锁,线程尝试获取锁时,如果锁已经被其他线程占用,它会不断地循环尝试获取锁,而不会被挂起。
    • 自旋锁适用于对共享资源的竞争时间较短,且线程等待时间较短的情况。

    优点:

    • 在线程竞争较少且锁竞争时间短的情况下,自旋锁效率高,不会引发线程挂起和上下文切换的开销。
    • 可以避免多线程上下文切换带来的性能开销。

    缺点:

    • 在线程竞争激烈时,自旋锁可能会导致 CPU 资源被大量消耗,产生性能问题。
    • 自旋锁适用于线程等待时间短的情况,不适用于长时间等待的情况,因为它会浪费 CPU 时间。

在工程上,最常用的多线程优化手段包括:

  1. 减少锁竞争

    • 这是最常见的多线程优化手段之一。通过使用更细粒度的锁、减少共享资源、使用无锁数据结构或者使用更高级的同步机制,可以减少线程之间的锁竞争,提高多线程应用程序的性能。
  2. 使用线程池

    • 线程池是一种常用的多线程优化方式。通过创建线程池,可以重用线程,减少线程的创建和销毁开销,提高多线程应用程序的效率。
  3. 使用并发数据结构

    • 并发数据结构是线程安全的数据结构,如并发队列、并发哈希表等。它们允许多个线程同时访问共享数据而不需要额外的锁。使用这些数据结构可以减少锁竞争,提高多线程应用程序的性能。
文章来源:https://blog.csdn.net/Algo_x/article/details/135323754
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。