最近在复习这块内容,随时整理一下本菜菜的笔记,希望也对您有帮助,未完。首先,着重感谢一下Guide哥的公众号,还有大神整理的技能树,以及小姐姐wy9分享的知识整理(其他参考资料也是在里面直接贴的超链接~)欢迎大家讨论和指正,谢谢٩(๑>◡<๑)۶
文章目录
- 一、操作系统基础
- 1.1 什么是操作系统?
- 1.2 系统调用
- 1.3 处理机
- 1.4 中央处理器(CPU,Central Processing Unit):
- 1.5 内核
- 1.6 操作系统四个特性
- 并发并行
- 1.7 操作系统的目标和功能
- 二、程序、进程、线程
- 2.1 进程与线程比较
- 1 区别:
- 2 线程和进程操作中主要的接口
- 3 线程基本状态
- 进程基本状态
- 2.2多线程
- 1 线程概念
- 2 什么时候使用多线程?
- 3 为啥使用多线程?
- 4 线程的创建和结束
- 5 线程属性值修改
- 6 多线程的同步互斥
- 1)临界区CriticalSection:
- 2)互斥量(互斥锁)Mutex:
- 3)条件变量
- 4)自旋锁
- 5)读写锁
- 6)屏障
- 7)信号量Semaphore:(同步or互斥、可跨进程)
- 8)事件(信号)Event:(同步or互斥、可跨进程)
- 7 Linux下同步机制?
- 2.3 多进程
- 1 进程相关概念
- 2 进程的创建和结束
- 3 进程间的通信方式
- 1)管道(Pipes)
- 匿名管道(Pipes) :
- 有名管道(Names Pipes) :
- 2)信号(Signal) :
- 3)消息队列(Message Queuing) :
- 4)信号量(Semaphores) :
- 5)共享内存(Shared memory) :
- 6)套接字(Sockets) :
- 4 进程的调度算法
- 5 进程死锁
- 1)死锁发生的必要条件
- 2)死锁处理
- 6 回收子进程
- 1)僵尸进程、孤儿进程
- 概念知识
- 相关接口
- 2)守护进程
- 概念:
- 操作步骤:
- 三、内存管理基础
- 3.1 内存管理介绍
- 3.2 内存管理机制
- 3.3 快表和多级页表
- 快表(TLB)
- 多级页表
- 3.4 分页机制和分段机制的共同点和区别
- 3.5 逻辑(虚拟)地址和物理地址
- 3.6 CPU寻址了解吗?为什么需要虚拟地址空间?
- 1 虚拟寻址 (Virtual Addressing)
- 2 为什么要有虚拟地址空间呢?
- 四 虚拟内存
- 1 进程地址空间
- a. 内核空间、用户空间
- b. 栈
- 2 进程控制块(处理机)
- 3上下文切换
- 4 exec函数
我通过以下四点向您介绍一下什么是操作系统吧!
关于内核:内核负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。内核就是操作系统背后黑盒的核心。
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的内核态级别的子功能咋办呢?那就需要系统调用了!
也就是说在我们运行的用户程序中,凡是与内核态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
这些系统调用按功能大致可分为如下几类:
计算机系统中存储程序和数据,并按照程序规定的步骤执行指令的部件。包括中央处理器、主存储器、I/O接口,处理器+外围设备(鼠标键盘之类)构成完整的操作系统
是一块超大规模的集成电路,是一台计算机的运算核心和控制核心。它的功能主要是解释计算机指令以及处理计算机软件中的数据。
操作系统的最基本部分、核心,决定一个程序在什么时候对某部分硬件操作多长时间
提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性
并发:同一段时间内多个程序执行(与并行区分,并行指的是同一时刻有多个事件,多处理器系统可以使程序并行执行)
共享:系统中的资源可以被内存中多个并发执行的进线程共同使用
虚拟:通过分时复用(如分时系统)以及空分复用(如虚拟内存)技术把一个物理实体虚拟为多个
异步:系统进程用一种走走停停的方式执行,(并不是一下子走完),进程什么时候以怎样的速度向前推进是不可预知的
并发并行
- 一个逻辑流的执行在时间上与另一个流重叠,称为并发流,它们并发地执行的一般现象就叫做并发(concurrency),并发流的思想与流运行的处理器核数或者计算机数无关。
- 如果两个流并发地运行在不同的处理器核或计算机上,就称它们为并行流(parallel flow)
- 并发:更多的是针对逻辑结构,不同任务可以同时开始。在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一个时刻点上仍只有一个进程在运行。(时钟中断)
并行:是多个任务可以同时在CPU上执行,并行更关注执行状态。
参考资料
处理机管理:
存储器管理:
文件管理:
设备管理:
- 程序(program):为完成特定任务、用某种语言编写的一组指令。即指一段静态的代码。在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)
- 程序运行起来,产生一个进程
- 进程(process):进程是动态的,是程序的一次执行过程,或是正在运行的一个程序。占用系统资源,在内存中执行。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
- 线程(thread):进程可进一步细化为线程,线程是进程中的一个执行单元,是一个程序内部的一条执行路径。一个进程中至少有一个线程。
项目 | 线程 | 进程 | 其他 |
---|---|---|---|
关系 | 一个线程只能属于一个进程 | 一个进程可以有多个线程(至少一个) | 线程依赖于进程而存在 |
内存 | 资源分配给进程,同一个进程的所有线程共享进程的所有资源(代码段,数据段,扩展段),每个线程拥有自己的栈段(存放局部变量和临时变量),每个线程都独占一个虚拟处理器(独自的寄存器组、指令计数器、处理器状态) | 进程在执行过程中拥有独立的内存单元 | 代码段:代码、常量,数据段:全局变量、静态变量,扩展段:堆存储。 |
最小单位 | CPU调度的最小单位,是操作系统可识别的最小执行和调度单位 | 资源调度和分配的最小单位 | |
作用 | 用于保证程序的实时性,实现进程内部并发 | 实现操作系统的并发 | |
系统开销 | 小线程切换只需要保存和设置少量寄存器内容,不涉及存储器管理等 | 大创建和撤销进程:要分配or回收资源,切换:整个当前进程CPU环境的保存&新被调度的进程的CPU环境设置 | |
通信 | 同一个进程的多个线程有相同的地址空间,所以同步和通信容易,可能无序内核干预 | 进程间通信IPC | |
编译调试 | 开销小,切换速度快,调试复杂 | 编程调试简单可靠,创建销毁开销大 | |
牵连 | 同一进程中的线程极有可能会相互影响,一个线程挂掉将导致整个进程挂掉 | 基本上各进程是独立的,进程间不会相互影响 | |
适用 | 多核 | 多核、多机分布 |
【小结】:线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
fork()和pthread_create() 负责创建。
调用fork()后返回两次,一次标识主进程一次标识子进程;调用pthread_create()后得到一个可以独立执行的线程。
wait()和pthread_join() 负责回收。
调用wait()后父进程阻塞;调用pthread_join()后主线程阻塞。
exit()和pthread_exit() 负责退出。
调用exit()后调用进程退出,控制权交给系统;调用pthread_exit()后线程退出,控制权交给主线程。
多线程模型主要优势为线程间切换代价较小,因此适合I/O密集型工作场景,所以在编写程序时,遇到了阻塞过程而不想使整个程序停止响应时,应使用多线程。同时,多线程模型也适用于单机多核分布式场景。
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:通过地址传递来传递函数参数,这里是无符号类型指针,可以传任意类型变量的地址,在被传入函数中先强制类型转换成所需类型即可。 |
pthread_t pthread_self();
调用时,会打印线程ID。int pthread_join(pthread_t tid, void** retval);
pthread_exit( void *retval );
int pthread_detach(pthread_t tid);
pthread_detach(tid);
pthread_detach(pthread_self());
调用后和主线程分离,子线程结束时自己立即回收资源。线程属性对象类型为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***()
系统调用函数来设置和获取。这里不一一罗列。
在多任务操作系统中,同时运行的多个任务可能:
- 都需要访问/使用同一种资源;
- 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。
用户模式:不需要切换内核态,只在用户态完成操作。
互斥only、线程所有权:串行到谁谁可、不可跨进程
内核模式:利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。
✋综述:原子操作。采用对象互斥锁的概念,保证数据同一时间唯一访问。互斥量有两个状态,解锁、加锁。(线程所有权:拥有互斥对象的可)可跨进程,也可跨程序。
过程:
实现:
(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)
(1)按照参数cond_attr
指定属性创建一个条件变量。
调用成功:返回,并将条件变量ID赋值给参数cond
;否则返回错误代码。
#include
// 初始化条件变量
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);
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);
Linux下的线程同步:互斥锁,自旋锁,读写锁,屏障
Windows下的线程同步:互斥量,信号量,事件,关键代码段
学习资料:
互斥锁、条件变量、读写锁、自旋锁、信号量;
pthread.h相关函数说明;
多线程全面总结;
互斥量,读写锁,自旋锁,条件变量,屏障;
线程同步方式比较
进程相关知识参考资料
1) 进程是资源调度和分配的基本单位,实现了操作系统的并发
2) 进程由代码段、堆栈段、数据段构成:
3)父子进程共享所有数据:
fork()
一个子进程再通过execv()
重新加载新的代码段的过程。4)什么时候使用多进程?
1)进程有两种创建方式:
2)从计算机启动到终端执行程序的过程为:
3)从shell进程到创建其他子进程需要通过以下接口:
(1) 相关接口:
pid_t fork(void);
void exit(int status);
pid_t getpid(void);
返回调用者pid。pid_t getppid(void);
返回父进程pid。(2)正常退出方式:exit()、_exit()、return(在main中)
。
exit()
和 _exit()
区别:
return
和 exit()
区别:
(3)异常退出方式:abort();
、终止信号
管道(管道只能承载无格式字节流以及缓冲区大小受限。无名管道:亲缘关系,单项流动,半双工,内存文件;有名管道:任意进程,FIFO,磁盘文件)
信号(通知接收进程某个事件已经发生)
消息队列(消息的链表,存放在内核中并由消息队列标识符标识)
共享内存(由一个进程创建,但是多个进程可以访问。读写操作时需要用同步互斥的工具,保证在一个进程对这段内存进行访问的时候其他进程不能同时来)、
信号量(计数器,用来控制多个进程对资源的访问,它通常作为一种锁机制。)
套接字(支持TCP/IP的网络通信的基本操作单元)
【详细解释】
int pipe(int fd[2]);
int mkfifo(const char *pathname, mode_t mode);
信号量是一个计数器,用于多进程对共享数据的访问,常作为一种锁机制,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
相关接口
创建信号量:int semget(key_t key, int nsems, int semflag);
改变信号量值:int semop(int semid, struct sembuf *sops, unsigned nsops);
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
};
直接控制信号量信息:int semctl(int semid, int semnum, int cmd, union semun arg);
使得多个进程可以访问同一块内存空间;如果某个进程向共享内存内写入数据,所做的改动将立即影响到可以访问该共享内存的其他所有进程,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
相关接口
int shmget(key_t key, int size, int flag);
void *shmat(int shmid, void *addr, int flag);
int shmdt(const void *shmaddr);
其他补充
共享内存的方式像极了多线程中线程对全局变量的访问,大家都对等地有权去修改这块内存的值,这就导致在多进程并发下,最终结果是不可预期的。所以对这块临界区的访问需要通过信号量来进行进程同步。
但共享内存的优势也很明显,首先可以通过共享内存进行通信的进程不需要像无名管道一样需要通信的进程间有亲缘关系。其次内存共享的速度也比较快,不存在读取文件、消息传递等过程,只需要到相应映射到的内存地址直接读写数据即可。
此方法主要用于在客户端和服务器之间通过网络通信。套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
多个并发的进程中,如果每个进程都持有某种资源,等待其他进程释放它现在保持的资源。这些资源都只允许一个进程占用,结果两个进程都不能继续执行,也不会释放自己占有的资源。所以这种双方循环等待的现象会无限期持续,发生死锁。
父进程在调用 fork 接口之后和子进程已经可以独立开,之后父进程和子进程就以未知的顺序向下执行(异步过程)。所以父进程和子进程都有可能先执行完。
init
进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素。wait
或者waitpid
获取进程状态信息,那么子进程描述符就会一直保存在系统中,这种进程称为僵尸进程。回收进程(1)
pid_t wait(int *status);
回收进程(2)
pid_t waitpid( pid_t pid, int *status, int options );
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
ret=waitpid(-1,NULL,0);
// 不想使用它们,也可以把options设为0守护进程是脱离终端并在后台运行的进程,执行过程中信息不会显示在终端上,并且也不会被终端发出的信号打断。
fork() + if(pid > 0){exit(0);}
,使子进程称为孤儿进程,被init进程收养。setsid()
。chdir("/")
。umask(0)
。for(int i = 0; i < 65535; ++i){close(i);}
。操作系统的内存管理主要是做什么?
操作系统的内存管理主要负责:
连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。
同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
在分页内存管理中,很重要的两点是:
- 虚拟地址到物理地址的转换要快。
- 解决虚拟地址空间大,页表也会很大的问题。
我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。
物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。
使用虚拟寻址,CPU需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。
实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件。如下图所示:
如果直接把物理地址暴露出来会带来问题:比如可能对操作系统造成伤害,以及给同时运行多个程序造成困难。
通过虚拟地址访问内存有以下优势:
—未完待续—
虚拟存储器为每个进程提供了独占系统地址空间的假象。
Linux的虚拟地址空间范围为0~4GB(使用了一部分硬盘当做内存来使用)。
【1】Linux内核将4G字节的空间分为两部分:
【2】通常情况下代码运行在用户态(使用用户地址空间),当发生系统调用、进程切换等操作时,CPU控制寄存器设置模式位,进入内核模式:
【3】每个进程都运行在一个属于它的虚拟地址空间中:
进程的调度:实际就是内核选择相应的进程控制块,被选择的进程控制块中包含了一个进程基本的信息。
内核管理所有进程控制块,而进程控制块记录了进程全部状态信息。每一次进程调度就是一次上下文切换,所谓的上下文本质上就是当前运行状态,主要包括通用寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈和内核数据结构(页表、进程表、文件表)等。进程执行时刻,内核可以决定抢占当前进程并开始新的进程,这个过程由内核调度器完成,当调度器选择了某个进程时称为该进程被调度,该过程通过上下文切换来改变当前状态。一次完整的上下文切换通常是进程原先运行于用户态,之后因系统调用或时间片到切换到内核态执行内核指令,完成上下文切换后回到用户态,此时已经切换到进程B。