来聊聊操作系统吧

最近在复习这块内容,随时整理一下本菜菜的笔记,希望也对您有帮助,未完。首先,着重感谢一下Guide哥的公众号,还有大神整理的技能树,以及小姐姐wy9分享的知识整理(其他参考资料也是在里面直接贴的超链接~)欢迎大家讨论和指正,谢谢٩(๑>◡<๑)۶

文章目录

一、操作系统基础

1.1 什么是操作系统?

我通过以下四点向您介绍一下什么是操作系统吧!

  1. 操作系统(Operating System,简称OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基石;
  2. 操作系统本质上是运行在计算机上的软件程序
  3. 操作系统为用户提供一个与系统交互的操作界面
  4. 操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核可以理解为能直接操作硬件的程序)。

💗关于内核:内核负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。内核就是操作系统背后黑盒的核心。
操作系统分内核和外壳


1.2 系统调用

根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

  • 用户态(user mode) :用户态运行的进程或可以直接读取用户程序的数据
  • 内核态(kernel mode、系统态):可以简单的理解内核态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的内核态级别的子功能咋办呢?那就需要系统调用了!
也就是说在我们运行的用户程序中,凡是与内核态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

这些系统调用按功能大致可分为如下几类:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
    文件管理。完成文件的读、写、创建及删除等功能。
    进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
    进程通信。完成进程之间的消息传递或信号传递等功能。
    内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

1.3 处理机

计算机系统中存储程序和数据,并按照程序规定的步骤执行指令的部件。包括中央处理器、主存储器、I/O接口,处理器+外围设备(鼠标键盘之类)构成完整的操作系统

  • 程序是描述处理机完成某项任务的指令序列。
  • 指令则是处理机能直接解释、执行的信息单位。

1.4 中央处理器(CPU,Central Processing Unit):

是一块超大规模的集成电路,是一台计算机的运算核心和控制核心。它的功能主要是解释计算机指令以及处理计算机软件中的数据


1.5 内核

操作系统的最基本部分、核心,决定一个程序在什么时候对某部分硬件操作多长时间

提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性


1.6 操作系统四个特性

并发:同一段时间内多个程序执行(与并行区分,并行指的是同一时刻有多个事件,多处理器系统可以使程序并行执行)

共享:系统中的资源可以被内存中多个并发执行的进线程共同使用

虚拟:通过分时复用(如分时系统)以及空分复用(如虚拟内存)技术把一个物理实体虚拟为多个

异步:系统进程用一种走走停停的方式执行,(并不是一下子走完),进程什么时候以怎样的速度向前推进是不可预知的

并发并行

  • 一个逻辑流的执行在时间上与另一个流重叠,称为并发流,它们并发地执行的一般现象就叫做并发(concurrency),并发流的思想与流运行的处理器核数或者计算机数无关。
  • 如果两个流并发地运行在不同的处理器核或计算机上,就称它们为并行流(parallel flow)
  • 并发:更多的是针对逻辑结构,不同任务可以同时开始。在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一个时刻点上仍只有一个进程在运行。(时钟中断)
    并行:是多个任务可以同时在CPU上执行,并行更关注执行状态。
    👉参考资料

1.7 操作系统的目标和功能

  • 处理机管理:

    • 处理机的运行以进程(或线程)为基本单位,对处理机的管理可归结为对进程的管理。
    • 管理进程的资源共享:进程控制、进程同步、进程通信、死锁处理、处理机调度
  • 存储器管理:

    • 给多道程序的运行提供良好环境,方便用户使用+提高内存利用率
    • 内存分配、地址映射、内存保护与共享、内存扩充
  • 文件管理:

    • 计算机中的信息以文件形式存在。
    • 文件存储空间管理、目录管理、文件读写管理和保护
  • 设备管理:

    • 完成用户的I/O请求,方便用户使用各种设备,并提高设备利用率
    • 缓冲管理、设备分配、设备处理、虚拟设备

二、程序、进程、线程

  • 程序(program):为完成特定任务、用某种语言编写的一组指令。即指一段静态的代码。在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)
  • 程序运行起来,产生一个进程
  • 进程(process):进程是动态的,是程序的一次执行过程,或是正在运行的一个程序。占用系统资源,在内存中执行。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
  • 线程(thread):进程可进一步细化为线程,线程是进程中的一个执行单元,是一个程序内部的一条执行路径。一个进程中至少有一个线程。

2.1 进程与线程比较

1 区别:

项目线程进程其他
关系一个线程只能属于一个进程一个进程可以有多个线程(至少一个)线程依赖于进程而存在
内存资源分配给进程,同一个进程的所有线程共享进程的所有资源(代码段,数据段,扩展段),每个线程拥有自己的栈段(存放局部变量和临时变量),每个线程都独占一个虚拟处理器(独自的寄存器组、指令计数器、处理器状态)进程在执行过程中拥有独立的内存单元代码段:代码、常量,数据段:全局变量、静态变量,扩展段:堆存储。
最小单位CPU调度的最小单位,是操作系统可识别的最小执行和调度单位资源调度和分配的最小单位
作用用于保证程序的实时性,实现进程内部并发实现操作系统的并发
系统开销线程切换只需要保存和设置少量寄存器内容,不涉及存储器管理等创建和撤销进程:要分配or回收资源,切换:整个当前进程CPU环境的保存&新被调度的进程的CPU环境设置
通信同一个进程的多个线程有相同的地址空间,所以同步和通信容易,可能无序内核干预进程间通信IPC
编译调试开销小,切换速度快,调试复杂编程调试简单可靠,创建销毁开销大
牵连同一进程中的线程极有可能会相互影响,一个线程挂掉将导致整个进程挂掉基本上各进程是独立的,进程间不会相互影响
适用多核多核、多机分布

【小结】:线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

2 线程和进程操作中主要的接口

  • fork()和pthread_create() 负责创建。
    调用fork()后返回两次,一次标识主进程一次标识子进程;调用pthread_create()后得到一个可以独立执行的线程。

  • wait()和pthread_join() 负责回收。
    调用wait()后父进程阻塞;调用pthread_join()后主线程阻塞。

  • exit()和pthread_exit() 负责退出。
    调用exit()后调用进程退出,控制权交给系统;调用pthread_exit()后线程退出,控制权交给主线程。

3 线程基本状态

  • 创建:new Thread®创建,有了相应的内存空间和其他资源,但还未开始执行
  • 就绪状态,指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;
  • 运行状态,指线程占有处理机正在运行;
  • 阻塞状态,指线程在等待一个事件(如信号量),逻辑上不可执行。
  • 终止:stop()、destory()或run()结束后,不在具有继续运行的能力

进程基本状态

  • 创建状态(new) :进程正在被创建,尚未到就绪状态。
  • 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
  • 运行状态(running) :进程正在处理器上运行(单核CPU下任意时刻只有一个进程处于运行状态)。
  • 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行,如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
  • 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
    在这里插入图片描述

2.2多线程

1 线程概念

  • 多线程:同一个进程内部有多个线程。
  1. 所有的线程共享同一个进程的内存空间(同样的动态内存、映射文件、目标代码……),进程中定义的全局变量会被所有的线程共享,比如有全局变量int i = 10,这一进程中所有并发运行的线程都可以读取和修改这个i的值。
  2. 除了标识线程的tid,每个线程还有自己独立的栈空间,线程彼此之间是无法访问其他线程栈上内容的。每个线程独自占用一个虚拟处理器
  3. 线程调度只需要保存线程栈、寄存器数据和指令指针(PC)即可,相比进程切换开销要小很多,线程是处理机调度的最小单位,是程序执行流的最小单元
  4. 线程包括两个方面:第一:线程内核对象(OS用来存放统计信息的地方);第二:线程堆栈(函数参数和局部变量)
  5. 线程在它的进程的地址空间执行代码。内核对象句柄依赖于进程而存在。

2 什么时候使用多线程?

多线程模型主要优势为线程间切换代价较小,因此适合I/O密集型工作场景,所以在编写程序时,遇到了阻塞过程而不想使整个程序停止响应时,应使用多线程。同时,多线程模型也适用于单机多核分布式场景

3 为啥使用多线程?

  • 提高cpu资源利用率(执行主线程任务时,还可以同时执行其他的)
  • 创建线程开销比较小(比创建进程要小)
  • 线程之间可以共享数据(进程之间不行)

4 线程的创建和结束

  1. 创建线程:
int pthread_create( pthread_t *pthread, 
                    const pthread_attr_t *attr, 
                    void *(*start_routine)(void *),
                    void *agr
                  );
参数说明
pthread_t *pthread,pthread:用来返回线程的tid,标识线程,*pthread值即为tid,类型pthread_t == unsigned long int。
const pthread_attr_t *attr,attr:指向线程属性结构体的指针,用于改变所创线程的属性,填NULL使用默认值。
void *(*start_routine)(void *),定义了一个名字为start_routine的函数指针,指向返回类型是void* 类型(指针)且形参为void* 类型的函数。即:线程执行函数的首地址,传入函数指针。
void *arg);arg:通过地址传递来传递函数参数,这里是无符号类型指针,可以传任意类型变量的地址,在被传入函数中先强制类型转换成所需类型即可。
  1. 获得线程ID:
    pthread_t pthread_self(); 调用时,会打印线程ID。
  2. 等待线程结束:
    int pthread_join(pthread_t tid, void** retval);
    • 主线程调用:等待子线程退出并回收其资源,类似于进程中wait/ waitpid回收僵尸进程,调用pthread_join的线程会被阻塞
    • pthread_t tid,tid:创建线程时通过指针得到tid值。
    • void** retval,retval:指向返回值的指针。
  3. 结束线程:
    pthread_exit( void *retval );
    • 子线程执行,用来结束当前线程;并通过retval传递返回值,该返回值可通过pthread_join获得。
  4. 分离线程:
    int pthread_detach(pthread_t tid);
    • 主线程可以调用:pthread_detach(tid);
    • 子线程可以调用:pthread_detach(pthread_self());调用后和主线程分离,子线程结束时自己立即回收资源。

5 线程属性值修改

线程属性对象类型为pthread_attr_t,结构体定义如下:

typedef struct{
    int etachstate;    // 线程分离的状态
    int schedpolicy;    // 线程调度策略
    struct sched_param schedparam;    // 线程的调度参数
    int inheritsched;    // 线程的继承性
    int scope;    // 线程的作用域
    // 以下为线程栈的设置
    size_t guardsize;    // 线程栈末尾警戒缓冲大小
    int stackaddr_set;    // 线程的栈设置
    void *    stackaddr;    // 线程栈的位置
    size_t stacksize;    // 线程栈大小
}pthread_arrt_t;

对上述结构体中各参数大多有:pthread_attr_get***()和pthread_attr_set***()系统调用函数来设置和获取。这里不一一罗列。

6 多线程的同步互斥💗

在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源;
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
  • 同步:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。
    • 比如:A 任务的运行依赖于 B 任务产生的数据。
  • 互斥:当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。
    • 一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

用户模式:不需要切换内核态,只在用户态完成操作。

1)临界区CriticalSection:

互斥only、线程所有权:串行到谁谁可、不可跨进程

  • 适合一个进程内的多线程访问公共区域或代码段时使用。
    通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

内核模式:利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。

2)互斥量(互斥锁)Mutex:

综述:原子操作。采用对象互斥锁的概念,保证数据同一时间唯一访问。互斥量有两个状态,解锁、加锁。(线程所有权:拥有互斥对象的可)可跨进程,也可跨程序。

  • 过程:

    • 线程在访问共享资源后、临界区域前,对互斥锁进行加锁。线程访问完成后释放该锁。
    • 其他企图加锁的其他线程,将会被挂起。直到该锁被释放,被挂起的线程被唤醒并继续执行、锁定该互斥量。
  • 实现:

    • 创建和销毁

    (1)静态方式创建:pthread_mutex_t:结构,右值:结构常量

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    

    (2)动态方式创建:mutexattr用于指定互斥锁属性,如果为NULL则使用缺省属性。

    int pthread_mutex_init( pthread_mutex_t *mutex, 
                            const pthread_mutexattr_t *mutexattr ) 
    

    (3)注销互斥锁:释放锁所占用的资源,且要求锁当前处于开放状态。

    int pthread_mutex_destroy( pthread_mutex_t *mutex ) 
    

    注:在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

    • 互斥锁属性
      互斥锁的属性在创建锁的时候指定。
    • 锁的操作
    int pthread_mutex_lock(pthread_mutex_t *mutex)    // 加锁(在锁已经被占据时挂起)
    int pthread_mutex_unlock(pthread_mutex_t *mutex)  // 解锁
    int pthread_mutex_trylock(pthread_mutex_t *mutex) // 测试加锁(在锁已经被占据时返回EBUSY)
    
3)条件变量
  • 条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步 的一种机制,主要包括两个动作:
    • 一个线程等待"条件变量的条件成立"而挂起;
    • 另一个线程使 “条件成立”(给出条件成立信号)。
  • 原理:
      条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量
      如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。
      如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

(1)按照参数cond_attr指定属性创建一个条件变量。
调用成功:返回,并将条件变量ID赋值给参数cond;否则返回错误代码。

#include <pthread.h>
// 初始化条件变量
int pthread_cond_init( pthread_cond_t *cond,
				   pthread_condattr_t *cond_attr );

(2)为参数mutex指定的互斥量解锁,等待一个事件(由cond指定的条件变量)发生。调用本函数的线程被阻塞直到有其他的线程调用pthread_cond_signal或者pthread_cond_broadcast 置相应的条件变量并获得mutex互斥量时才解除。

// 阻塞等待,要在mutex锁定的区域内使用
int pthread_cond_wait( pthread_cond_t *cond, 
					  pthread_mutex_t *mutex );

(3) 当系统时间到达abstime参数指定的时间时,被阻塞的线程可以被唤起,继续执行。

// 超时等待,要在mutex锁定区域内使用
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex *mutex, const timespec *abstime);

(4)对所有等待cond指定的条件变量 的线程 解除阻塞

// 唤醒等待该条件的所有线程,最好在mutex锁定区域外调用
int pthread_cond_broadcast(pthread_cond_t *cond);  

(5)解除一个等待cond所指条件变量的线程的阻塞。当有多个线程挂起等待该条件变量时,也只唤醒一个线程。

// 只唤醒一个线程,最好在mutex锁定区域外调用
int pthread_cond_signal(pthread_cond_t *cond);

(6)释放一个条件变量cond,释放为它分配的资源。

int pthread_cond_destroy(pthread_cond_t *cond);
4)自旋锁
  • 和互斥量阻塞了线程不同,在获得锁之前一直处于忙等(自旋)阻塞状态
  • 场景:自旋锁只能被短时间持有。
5)读写锁
  • 顾名思义,将对共享资源的访问者分为读者、写者。读写锁有三种状态:读模式下加锁,写模式下加锁,不加锁装填。
  • 一个读写锁同时只能有一个写者or多个读者(与CPU数相关),但不能同时既有读者又有写者。
  • 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。
  • 如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
  • 场景:读写锁适用于对数据结构的读次数远大于写次数的情况。
    pthread_rwlock_t myrw;
    pthread_rwlock_init(&myrw, NULL);
    pthread_rwlock_rdlock(&myrw);//只读模式去加锁 
    pthread_rwlock_wrlock(&myrw);//只写模式去加锁    
    pthread_rwlock_unlock(&myrw); 
    pthread_rwlock_destroy(&myrw);
    
6)屏障
  • 让多个线程并行工作,然后所有参与的线程都到达一个屏障后,从该点继续执行。比如:主线程中设一个屏障,它一直等待:6个工作线程并行处理数据,当6组都处理完成后,主线程对这些所有的数据继续操作。
  • 场景:适用于并发完成同一项任务。
7)信号量Semaphore:(同步or互斥、可跨进程)
  • 它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
  • 与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据。先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。
8)事件(信号)Event:(同步or互斥、可跨进程)
  • 通过线程间触发事件实现同步互斥。
  • 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

Linux下的线程同步:互斥锁,自旋锁,读写锁,屏障
Windows下的线程同步:互斥量,信号量,事件,关键代码段

7 Linux下同步机制?

  • POSIX信号量:可用于进程同步,也可用于线程同步。
  • POSIX互斥锁 + 条件变量:只能用于线程同步。

👆 学习资料:
互斥锁、条件变量、读写锁、自旋锁、信号量
pthread.h相关函数说明
多线程全面总结
互斥量,读写锁,自旋锁,条件变量,屏障
线程同步方式比较


2.3 多进程

进程相关知识参考资料

1 进程相关概念

1) 进程是资源调度和分配的基本单位,实现了操作系统的并发
2) 进程由代码段、堆栈段、数据段构成:

  • 代码段:多个程序可以共享的二进制代码。
  • 创建新进程需要复制整个PCB,之后操作系统将PCB放到进程堆栈段底部。

3)父子进程共享所有数据:

  • 父进程创建子进程后,除了pid,其他几乎都一样。
  • 子进程在创建时,拷贝父进程PCB(进程控制块)的大部分内容。(包含各种数据、代码的地址、索引表地址)
  • 子进程读写数据时:会将公共数据拷贝一份,在其上操作。
  • 若子进程想运行自己的代码段,可以使用execv()函数重新加载新的代码段,将其与父进程独立开来
  • 在shell中执行程序:就是通过shell进程先fork()一个子进程再通过execv()重新加载新的代码段的过程。

4)什么时候使用多进程?

  • 适合CPU密集型,适用多机分布式场景,易于多机扩展。

2 进程的创建和结束

1)进程有两种创建方式:

  • 一种是操作系统创建的,一种是父进程创建的。

2)从计算机启动到终端执行程序的过程为:

  • 0号进程 -> 1号内核进程 -> 1号用户进程(init进程) -> getty进程 -> shell进程 -> 命令行执行进程。
  • 为什么shell一关闭,在shell中执行的进程都自动被关闭?
    在命令行中通过 ./program执行可执行文件时,所有创建的进程都是shell进程的子进程。

3)从shell进程到创建其他子进程需要通过以下接口:
(1) 相关接口:

  • 创建进程:pid_t fork(void);
    • 返回值:出错返回-1;父进程中返回pid > 0;子进程中pid == 0
  • 结束进程:void exit(int status);
    • status 是退出状态,保存在全局变量中S?,通常0表示正常退出。
    • 获得PID:pid_t getpid(void); 返回调用者pid。
    • 获得父进程PID:pid_t getppid(void); 返回父进程pid。

(2)正常退出方式:exit()、_exit()、return(在main中)

  • exit()_exit()区别:
    • exit() 是对 _exit() 的封装,都会终止进程并做相关收尾工作,
    • 最主要的区别是 _exit() 函数关闭全部描述符和清理函数后,不会刷新流
      但是 exit() 会在调用 _exit() 函数前,刷新数据流
  • returnexit() 区别:
    • exit():是函数,但有参数,执行完之后控制权交给系统
    • return:若是在调用函数中,执行完之后控制权交给调用进程,若是在main函数中,控制权交给系统

(3)异常退出方式:abort();终止信号

3 进程间的通信方式💗

管道(管道只能承载无格式字节流以及缓冲区大小受限。无名管道:亲缘关系,单项流动,半双工,内存文件;有名管道:任意进程,FIFO,磁盘文件)
信号(通知接收进程某个事件已经发生)
消息队列(消息的链表,存放在内核中并由消息队列标识符标识)
共享内存(由一个进程创建,但是多个进程可以访问。读写操作时需要用同步互斥的工具,保证在一个进程对这段内存进行访问的时候其他进程不能同时来)、
信号量(计数器,用来控制多个进程对资源的访问,它通常作为一种锁机制。)
套接字(支持TCP/IP的网络通信的基本操作单元)

【详细解释】

1)管道(Pipes)
匿名管道(Pipes) :
  • 无名管道特点: (内存文件)
    • 用于具有亲缘关系的父子进程间或者兄弟进程之间的通信
    • 数据只能单向流动,是半双工方式,如果双方需要同时收发数据需要两个管道。
  • 相关接口:int pipe(int fd[2]);
    • fd[2]:管道两端用fd[0]和fd[1]来描述,读的一端用fd[0]表示,写的一端用fd[1]表示。
    • 通信双方的进程中写数据的一方需要把 fd[0] (读) 先close掉,读的一方需要先把 fd[1] (写) 给close掉。
有名管道(Names Pipes) :
  • 有名管道特点:(FIFO文件,借助文件系统)
    • 有名管道是FIFO文件,严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以通过文件路径名来指出。
    • 可以实现本机任意两个进程通信
  • 相关接口:int mkfifo(const char *pathname, mode_t mode);
    • pathname:即将创建的FIFO文件路径,如果文件存在需要先删除。
    • mode:和open()中的参数相同。
2)信号(Signal) :
  • 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;比如按下ctrl + C就是信号。
3)消息队列(Message Queuing) :
  • 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。
  • 消息队列存放在内核中,只有在内核重启(即,操作系统重启)
    或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
    【对比】
    • 无名管道:只存在于内存中的文件;
    • 命名管道:存在于实际的磁盘介质或者文件系统;
    • 消息队列:存在于内核。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比FIFO更有优势。
  • 消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺陷。
4)信号量(Semaphores) :

信号量是一个计数器,用于多进程对共享数据的访问,常作为一种锁机制,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
相关接口

创建信号量:int semget(key_t key, int nsems, int semflag);

  • 创建成功返回信号量标识符,失败返回-1。
    key:进程pid。
    nsems:创建信号量的个数。
    semflag:指定信号量读写权限。

改变信号量值:int semop(int semid, struct sembuf *sops, unsigned nsops);

  • 我们所需要做的主要工作就是串讲sembuf变量并设置其值,然后调用semop,把设置好的sembuf变量传递进去。
    struct sembuf结构体定义如下:
struct sembuf{
    short sem_num;
    short sem_op;
    short sem_flg;
};
  • 成功返回信号量标识符,失败返回-1。
  • semid:信号量集标识符,由semget()函数返回。
    sops:指向struct sembuf结构的指针,先设置好sembuf值再通过指针传递。
    nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作。

直接控制信号量信息:int semctl(int semid, int semnum, int cmd, union semun arg);

  • semid:信号量集标识符。
    semnum:信号量集数组上的下标,表示某一个信号量。
    arg:union semun类型。
5)共享内存(Shared memory) :

使得多个进程可以访问同一块内存空间;如果某个进程向共享内存内写入数据,所做的改动将立即影响到可以访问该共享内存的其他所有进程,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。

  • 相关接口

    • 创建共享内存: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()返回的地址指针。
  • 其他补充
    共享内存的方式像极了多线程中线程对全局变量的访问,大家都对等地有权去修改这块内存的值,这就导致在多进程并发下,最终结果是不可预期的。所以对这块临界区的访问需要通过信号量来进行进程同步。
    但共享内存的优势也很明显,首先可以通过共享内存进行通信的进程不需要像无名管道一样需要通信的进程间有亲缘关系。其次内存共享的速度也比较快,不存在读取文件、消息传递等过程,只需要到相应映射到的内存地址直接读写数据即可。

6)套接字(Sockets) :

此方法主要用于在客户端和服务器之间通过网络通信。套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

4 进程的调度算法

  • 先来先服务 FCFS调度:按进程到达的先后顺序依次调度
  • 短作业优先 SJF调度:选择队列中估计时间较短的先进行处理
  • 时间片轮转:按进程到达的先后顺序放入队列,给队首进程分配CPU时间片,用完后计时器发出中断,暂停当前进程并将其放到队伍尾部,循环。
  • 多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。因而它是目前被公认的一种较好的进程调度算法,UNIX操作系统采取的便是这种调度算法。
  • 优先级调度 :为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以FCFS方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

5 进程死锁

多个并发的进程中,如果每个进程都持有某种资源,等待其他进程释放它现在保持的资源。这些资源都只允许一个进程占用,结果两个进程都不能继续执行,也不会释放自己占有的资源。所以这种双方循环等待的现象会无限期持续,发生死锁。

1)死锁发生的必要条件
  • 资源互斥使用:资源不能共享,只能一个进程用
  • 多个进程保持一定的资源,但又请求新的资源
  • 资源不可被剥夺:已经分配的资源不能从相应进程中强制剥夺
  • 多个进程循环等待:系统中若干进程形成环路,环路中的每个进程都在等待相邻进程正占用的资源
2)死锁处理
  • 死锁预防:如进程需要的所有资源,在一开始就全部申请好,得到之后再开始执行。
  • 死锁避免:在资源的动态分配中防止系统进入不安全状态。如进程每次申请申请资源的时候,根据一定的算法,去看该请求能不能造成死锁**,如果可能,就不给它分配该资源。
  • 死锁处理:破环四个原因中的一个或多个,但会影响到资源利用率及吞吐量。如:kill掉一个进程。
  • 死锁解除:对死锁相关进程,通过撤销或挂起的方式,释放一些资源。
  • 死锁忽略:不管死锁,由用户自行处理,比如重启电脑。一般的系统其实都采取这种策略。

6 回收子进程

1)僵尸进程、孤儿进程
概念知识

父进程在调用 fork 接口之后和子进程已经可以独立开,之后父进程和子进程就以未知的顺序向下执行(异步过程)。所以父进程和子进程都有可能先执行完。

  1. 孤儿进程:子进程没爹爹了,被收养
    当父进程先结束,子进程此时就会变成孤儿进程,不过这种情况问题不大,孤儿进程会自动向上被 init 进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素
  2. 僵尸进程:子进程死了却依然有他的传说
    如果子进程先结束,父进程并未调用wait或者waitpid获取进程状态信息,那么子进程描述符就会一直保存在系统中,这种进程称为僵尸进程。
相关接口

👉

  • 回收进程(1)

    	pid_t wait(int *status);
    
    • 一旦调用wait(),就会立即阻塞自己,wait()自动分析某个子进程是否已经退出。
      如果找到僵尸进程,就会负责收集和销毁;如果没有找到,就一直阻塞在这里。
    • status:指向子进程结束状态值。
  • 回收进程(2)

    	pid_t  waitpid( pid_t pid, int *status, int options );
    
    • 返回值:
      • 返回pid:返回收集的子进程id。
      • 返回-1:出错。
      • 返回0:没有被收集的子进程。
    • pid:子进程识别码,控制等待哪些子进程。
      • pid < -1,等待进程组识别码pid绝对值任何进程
        • (指定进程组的任何子进程)。
      • pid = -1,等待任何一个子进程退出,此时waitpid和wait的作用一模一样
        • (无限制)。
      • pid = 0,等待进程组识别码目前进程相同任何子进程
        • (等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬)。
      • pid > 0,等待子进程识别码pid任何子进程
        • (不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去)。
    • status:指向返回码的指针。
    • options:选项决定父进程调用 waitpid 后的状态。
      • ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
      • ret=waitpid(-1,NULL,0); // 不想使用它们,也可以把options设为0
      • 使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
      • options = WUNYRACED,子进程进入暂停马上返回,但结束状态不予理会。
2)守护进程
概念:

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

操作步骤:
  • 创建子进程,父进程退出:fork() + if(pid > 0){exit(0);},使子进程称为孤儿进程,被init进程收养。
  • 在子进程中创建新会话:setsid()
  • 改变当前目录结构为根:chdir("/")
  • 重设文件掩码:umask(0)
  • 关闭文件描述符:for(int i = 0; i < 65535; ++i){close(i);}

三、内存管理基础

3.1 内存管理介绍

操作系统的内存管理主要是做什么?
操作系统的内存管理主要负责:

  • 内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),
  • 地址转换也就是将逻辑地址转换成相应的物理地址等。

3.2 内存管理机制

连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。
同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。

  • 块式管理
    • 远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
  • **页式管理 **
    • 把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。
    • 页式管理通过页表对应逻辑地址和物理地址。
  • 段式管理
    • 页式管理其中的页实际并无任何实际意义。段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
    • 段式管理通过段表对应逻辑地址和物理地址。
  • 段页式管理机制
    • 把主存先分成若干段,每个段又分成若干页,也就是说段页式管理机制中段与段之间以及段的内部的都是离散的。

3.3 快表和多级页表

在分页内存管理中,很重要的两点是:

  • 虚拟地址到物理地址的转换要快。
  • 解决虚拟地址空间大,页表也会很大的问题。

快表(TLB)

  1. 作用:加速虚拟地址到物理地址的转换。
  2. 内容:页表的一部分or全部内容。(类似于高速缓存cache)
  3. 对比:采用页表做地址转换,读写内存数据时CPU要访问两次主存。使用快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
  4. 地址转换流程:
    (1)根据虚拟地址中的页号查快表
    (2)如果该页在快表中,直接从快表中读取相应的物理地址
    (3)如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中
    (4)当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。

多级页表

  1. 目的:避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。时间换空间。

3.4 分页机制和分段机制的共同点和区别

  1. 共同点 :
    分页机制和分段机制都是为了提高内存利用率,较少内存碎片
    页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的
  2. 区别 :
    页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
    分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。

3.5 逻辑(虚拟)地址和物理地址

我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。
物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。

3.6 CPU寻址了解吗?为什么需要虚拟地址空间?

1 虚拟寻址 (Virtual Addressing)

使用虚拟寻址,CPU需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。
实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件。如下图所示:
在这里插入图片描述

2 为什么要有虚拟地址空间呢?

如果直接把物理地址暴露出来会带来问题:比如可能对操作系统造成伤害,以及给同时运行多个程序造成困难。

通过虚拟地址访问内存有以下优势:

  • 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
  • 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。
    • 当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件
    • 数据或代码页会根据需要在物理内存与磁盘之间移动。
  • 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

四 虚拟内存

—未完待续—

1 进程地址空间

虚拟存储器为每个进程提供了独占系统地址空间的假象。

  • 用户空间中的文本区和数据区是通过存储器映射方式,将磁盘中可执行文件的相应段,映射至虚拟存储器地址空间中。
a. 内核空间、用户空间

Linux的虚拟地址空间范围为0~4GB(使用了一部分硬盘当做内存来使用)。
【1】Linux内核将4G字节的空间分为两部分:

  • 较低的3GB供各个进程使用,称为用户空间。(从虚拟地址0x00000000到0xBFFFFFFF)
  • 最高的1GB供内核使用,称为内核空间。(从虚拟地址0xC0000000到0xFFFFFFFF)

【2】通常情况下代码运行在用户态(使用用户地址空间),当发生系统调用、进程切换等操作时,CPU控制寄存器设置模式位,进入内核模式

  • 此时, Linux内核由系统内的所有进程共享,每个进程可以拥有4GB的虚拟空间,进程可以访问全部存储器位置和执行全部指令

【3】每个进程都运行在一个属于它的虚拟地址空间中:

  • 各自独立的私有用户空间(0~3GB,对系统中的其他进程是不可见)[存放的是用户程序的代码和数据]
  • 所有进程以及内核所共享的:虚拟内核空间(0~1GB)。[存放内核代码和数据]
    ——注:内核空间映射到物理内存是从最低地址(0x00000000)
    ——虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。
b. 栈

2 进程控制块(处理机)

进程的调度:实际就是内核选择相应的进程控制块,被选择的进程控制块中包含了一个进程基本的信息。

3上下文切换

内核管理所有进程控制块,而进程控制块记录了进程全部状态信息。每一次进程调度就是一次上下文切换,所谓的上下文本质上就是当前运行状态,主要包括通用寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈和内核数据结构(页表、进程表、文件表)等。进程执行时刻,内核可以决定抢占当前进程并开始新的进程,这个过程由内核调度器完成,当调度器选择了某个进程时称为该进程被调度,该过程通过上下文切换来改变当前状态。一次完整的上下文切换通常是进程原先运行于用户态,之后因系统调用或时间片到切换到内核态执行内核指令,完成上下文切换后回到用户态,此时已经切换到进程B。

4 exec函数

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值