【系统软件工程师面试】3. os部分
操作系统
0、系统调用
系统调用是从一个应用程序到内核的函数调用,出于安全考虑,它们使用了特定的机制,实际上你只是调用了内核的 API。“ 系统调用(system call)”
linux系统调用时为什么需要内核栈,不直接使用用户栈?
避免高特权(比如 内核态)栈内存不被低特权(比如 用户态)任意修改。为了内核数据安全,用户修改内核数据需要从用户态切换为内核态,由系统调用完成转换。
用户内存空间,每个用户进程都有各自独立的内存空间,保持彼此独立透明,互不干扰。
内核内存空间,内核线程间无需切换页表,共享内存空间。
反对某答案里讲的 线性地址和虚拟地址,线性地址和虚拟地址(线性上连续地址)其实指代同一件事。
可能要问,那为什么相同的虚拟地址在不同的进程空间会指向不同的物理地址?那是因为各个进程的映射表并不一样。
带 MMU 的CPU处理的数据地址,全都是虚拟地址;
经过 MMU 处理后的地址,到达北桥,为PA地址(linux称为PA,或者物理地址,但其实还不是真正的物理地址);
经过北桥,到其他IP核或外设,一般还会做地址映射,这才是真正访问内存硬件或其他外设的地址,称为dma地址(真正的物理地址);
其他所谓的 “xx地址” 基本都与以上定义重叠。
平时说的系统调用开销大,主要是相对于函数调用来说的。
对于一个函数调用,汇编层面上就是一个CALL或者JMP,存入EIP,载入新的EIP,执行;
系统调用:
0. SS, ESP, EFLAGS, CS, and EIP 等寄存器切换
1. 系统调用时需要从用户态切换到内核态,由于内核态的栈用的是内核栈,因此还需要进行栈的切换。
2. 栈切换回导致cache miss
3. 进行一些额外的检查(权限、有效性等)
1、进程、线程、协程
进程概念
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。
在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。
线程概念
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性
引入线程的好处
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。。
(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
进程和线程的关系
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)处理机分给线程,即真正在处理机上运行的是线程。
(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
2、进程间通信的方式?
(1)管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
(2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
(3)消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
(4)共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
(5)信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
(6)套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
协程:只是一个异步控制流,本质上是 async/await 异步操作的变形,跟什么线程切换之类的没关系。
3. 线程/进程切换
进程切换的过程:
1。执行完转换前的最后一条指令
2。保留当前进程/线程p1的context, 应该是保留在其PCB里
3。陷入内核,调用scheduler选择下一个要运行的进程/线程p2
4。从内核恢复, 恢复p2的context (从其PCB里读取)
5。开始执行p2的指令(肯定是从上次停下来的地方接着运行)
进程切换需要从用户态->内核态->用户态
进程切换比线程切换开销大是因为进程切换时要切页表,而且往往伴随着页调度,因为进程的数据段代码段要换出去,以便把将要执行的进程的内容换进来。
- 同一个进程中的线程间进行切换,差不多仅需要切换线程的上下文(如寄存器状态、线程栈等)
- 切换到已经映射好的进程,同样差多仅需要切换进程的上下文和页表切换
- 不同进程的线程之间切换,开销同上一条
隐藏开销: 上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
协程:线程确实比协程性能更好,因为线程能利用多核达到真正的并行计算,如果任务设计的好,线程能几乎成倍的提高你的计算能力。
说线程性能不好很多情况是没有设计好导致大量锁、切换、等待,这些都是应用层问题。
协程因为是非抢占式,需要用户自己释放使用权切换到其他协程,同一时间只有一个协程运行,只是单线程的能力。
说协程性能好的,其实真正的原因是因为瓶颈在IO上面,而这个时候真正发挥不了线程的作用。
协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器,里面保存了 CPU运行任务所需要的信息,协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。而且完全在用户态进行。
4、线程同步
多线程的同步
有了上面的基本函数还不足以完成本题的要求,为什么呢?因为题目要求按照ABCABC...的方式打印,而3个线程却在抢占资源,所以无法控制排列顺序。这时就需要用到多线程编程中的同步技术。
对于多线程编程来说,同步就是同一时间只允许一个线程访问资源,而其他线程不能访问。多线程有3种同步方式:
- 互斥锁
- 条件变量
- 读写锁
1.互斥锁
互斥锁是最基本的同步方式,它用来保护一个“临界区”,保证任何时刻只由一个线程在执行其中的代码。这个“临界区”通常是线程的共享数据。
下面三个函数给一个互斥锁上锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mptr); int pthread_mutex_trylock(pthread_mutex_t *mptr); int pthread_mutex_unlock(pthread_mutex_t *mptr); |
假设线程2要给已经被线程1锁住的互斥锁(mutex)上锁(即执行pthread_mutex_lock(mutex)),那么它将一直阻塞直到到线程1解锁为止(即释放mutex)。
如果互斥锁变量时静态分配的,通常初始化为常值PTHREAD_MUTEX_INITIALIZER,如果互斥锁是动态分配的,那么在运行时调用pthread_mutex_init函数来初始化。
2.条件变量
互斥锁用于上锁,而条件变量则用于等待,通常它都会跟互斥锁一起使用。
int pthread_cond_wait(pthread_cond_t *cptr,pthread_mutex_t *mptr); int pthread_cond_signal(pthread_cond_t *cptr); |
通常pthread_cond_signal只唤醒等待在相应条件变量上的一个线程,若有多个线程需要被唤醒呢,这就要使用下面的函数了:
int pthread_cond_broadcast(pthread_cond_t *cptr); |
3.读写锁
互斥锁将试图进入连你姐去的其他简称阻塞住,而读写锁是将读和写作了区分,读写锁的分配规则如下:
(1)只要没有线程持有某个给定的读写锁用于写,那么任意数目的线程可以持有该读写锁用于读;
(2)仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配该读写锁用于写。
int pthread_rwlock_rdlock(pthread_relock_t *rwptr); int pthread_rwlock_wrlock(pthread_relock_t *rwptr); int pthread_rwlock_unlock(pthread_relock_t *rwptr); |
pthread_cond_wait 为什么需要传递 mutex 参数
链接:https://www.zhihu.com/question/24116967/answer/26747608
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- wait()操作通常伴随着条件检测,如:
while(pass == 0) pthread_cond_wait(...);
- signal*()函数通常伴随着条件改变,如:
pass = 1; pthread_cond_signal(...)
// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0)
pthread_cond_wait(...);
pthread_mutex_unlock(mtx);
// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(...);
然后,我们假设wait()操作不会自动释放、获取锁,那么代码会变成这样:
// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0) {
pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);
}
pthread_mutex_unlock(mtx);
// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(cv);
久而久之,程序员发现unlock, just_wait, lock这三个操作始终得在一起。于是就提供了一个pthread_cond_wait()函数来同时完成这三个函数。
另外一个证据是,signal()函数是不需要传递mutex参数的,所以关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。
所以我的结论是:传递的mutex并不是为了防止wait()函数内部的Race Condition!而是因为调用wait()之前你总是获得了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),并且这个mutex在你调用wait()之前必须得释放掉,调用wait()之后必须得重新获取。
所以,pthread_cond_wait()函数不是一个细粒度的函数,却是一个实用的函数。
生产者消费者模型
1 class BoundedBlockingQueue { 2 public: 3 BoundedBlockingQueue(int capacity) 4 : cap_(capacity) { 5 6 } 7 8 void enqueue(int element) { 9 { 10 unique_lock<mutex> l(m_); 11 cv_.wait(l, [this]() { return q_.size() != cap_; }) ; 12 q_.emplace(element); 13 } 14 cv_.notify_all(); 15 } 16 17 int dequeue() { 18 int element; 19 { 20 unique_lock<mutex> l(m_); 21 cv_.wait(l, [this]() { return !q_.empty(); }) ; 22 element = q_.front(); q_.pop(); 23 } 24 cv_.notify_all(); 25 return element; 26 } 27 28 int size() { 29 unique_lock<mutex> l(m_); 30 return q_.size(); 31 } 32 33 private: 34 mutex m_; 35 condition_variable cv_; 36 queue<int> q_; 37 int cap_; 38 };