C++面试总结之操作系统(一):进程与线程

同步机制:

进程的同步信号量、管程、互斥

线程的同步信号量、互斥量、消息、条件变量

通信机制:

进程的通信管道、FIFO、消息队列、信号量、共享内存、SOCKET

1.进程和线程的基本概念

(1)进程(process)

狭义定义:进程就是一段程序的执行过程。

广义定义:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。进程是系统进行资源分配和调度的一个独立单元。

1)进程是一个实体,每个进程都有自己的地址空间,一般情况下,包含文本区域、数据区域、堆栈

2)进程是执行中的程序,程序是一个没有生命的实体,只有处理器赋予程序生命时,他才能成为一个活动的实体,我们称之为进程

3)进程本身不会运行,是线程的容器。线程不能单独执行,必须组成进程

(2)进程状态:

1)就绪:获取出CPU外的所有资源、只要处理器分配资源就可以马上执行

2)运行:获得处理器分配的资源,程序开始执行

3)阻塞:当程序条件不够的时候,需要等待提交满足的时候才能执行。

(3)线程

线程是进程的实体,是CPU调度和分派的基本单元。

1)在一个进程内部,要同时干多件事情,就需要同时运行多个子任务,我们把进程内的这些子任务叫做线程

2)多线程就是为了同步完成多项任务(在单个程序中同时运行多个线程完成不同的任务和工作),不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率 

3)线程是程序执行流的最小单元。一个标准的线程由当前的线程ID、当前指令指针、寄存器和堆栈组成 

(4)线程状态

1)就绪:指线程具备运行的所有条件,逻辑上可以运行,在等待处理机 

2)运行:指线程占用处理机正在运行 

3)阻塞:线程在等待一个事件,逻辑上不可执行 

(5)如果我们要同时执行多个任务怎么办? 

1)启动多个进程,每个进程虽然只有一个线程,但是多个进程可以一块执行多个任务 

2)启动一个进程,在一个进程内启动多个线程,这样多个线程也可以一块执行多个任务 

2.线程同步的方法(4种)

线程同步是指线程之间的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的操作系统资源(譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。

线程互斥是一种特殊的线程同步。实际上,互斥和同步对应着线程间通信发生的两种情况:

(1)当有多个线程访问共享资源而不使资源被破坏时;

(2)当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。

(1)互斥锁:

采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用。

A. 初始化互斥量

互斥量是一个pthread_mutex_t类型的变量,有两种初始化方法:

a. 用宏常量初始化:

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

b. 用函数初始化:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

mutex:互斥量结构指针
attr:互斥量的属性结构指针

B. 设置互斥量属性

#include <pthread.h>
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

attr:互斥量的属性结构指针
type:PTHREAD_MUTEX_NORMAL(默认属性),PTHREAD_MUTEX_ERRORCHECK(会进行错误检查,速度比较慢),PTHREAD_MUTEX_RECURSIVE(递归锁)。
THREAD_MUTEX_ADAPTIVE_NP(适应锁,仅等待解锁后重新竞争)

对于递归锁,同一个线程对一个递归锁加锁多次,会有一个锁计数器,解锁的时候也需要解锁这个次数才能释放该互斥量。

C. 加锁与解锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock()得不到锁会阻塞
int pthread_mutex_trylock()得不到锁会立即返回,并返回EBUSY错误。
pthread_mutex_timedlock()会根据时间来等待加锁,如果这段时间得不到锁会返回ETIMEDOUT错误!
pthread_mutex_unlock()

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

D. 销毁互斥量(锁必须是unlock状态否则返回EBUSY)

int pthread_mutex_destroy(pthread_mutex_t *mutex);

(2)信号量

它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。

当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。CSemaphore类对象保存了对当前访问某一个指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程数目。如果这个计数达到了零,则所有对这个CSemaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。

CSemaphore 类的构造函数原型及参数说明如下:

CSemaphore(
   LONG lInitialCount = 1,
   LONG lMaxCount = 1,
   LPCTSTR pstrName = NULL,
   LPSECURITY_ATTRIBUTES lpsaAttributes = NULL 
);

· lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值;
· lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目;
· 后两个参数在同一进程中使用一般为NULL,不作过多讨论;

一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数大于0,就可以发出信号量信号。如果为0,则放入一个队列中等待。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。

BOOL ReleaseSemaphore(HANDLE hSemaphore,  // hSemaphore:信号量句柄 
                      LONG lReleaseCount, // lReleaseCount:信号量计数值 
                      LPLONG lpPreviousCount // 参数一般为NULL
                    );

(3)消息 

允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

(4)条件变量(cond) 

条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。 

A.初始化条件变量

int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t * c_attr ); 

B. 等待条件成立, 释放锁,同时阻塞等待条件变量为真才行。

timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

激活条件变量。pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞

清除条件变量。无线程等待,否则返回EBUSY

int pthread_cond_destroy(pthread_cond_t *cond);

3. 如何创建进程,如何创建守护进程

Fork()函数

4.进程间通信的方法

消息传递:管道(pipe)、命名管道(FIFO)、消息队列、信号

同步:互斥锁、读写锁、记录上锁、信号量

共享内存

SOCKET

(1)管道

普通管道是半双工的通信方式,数据只能单项流动,并且只能在具有亲缘关系的进程间流动。命名管道也是半双工,它允许无亲缘关系的进程间通信。

当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

(2)消息队列

消息队列是消息的链表,存放在内核中,一个消息队列由一个标识符(即队列ID)来标识。

消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级

消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

#include <sys/msg.h>

// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在以下两种情况下,msgget将创建一个新的消息队列:

· 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
· key参数为IPC_PRIVATE。

函数msgrcv在读取消息队列时,type参数有下面几种情况:

type == 0,返回队列中的第一个消息;
type > 0,返回队列中消息类型为 type 的第一个消息;
type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。

可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。

(3)信号量

A. 理解PV: 
    P(S):①S=S-1;
            ②如果S>0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
    V(S):①S=S+1;
            ②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

B. 信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。

 一般来说,信号量S>0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S=0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

利用信号量和PV操作实现进程互斥的一般模型是:
进程P1             进程P2           ……          进程Pn
 ……                  ……                               ……
P(S);              P(S);                         P(S);
临界区;             临界区;                        临界区;
V(S);              V(S);                        V(S);
 ……                   ……           ……            ……

(4)信号

用于通知接收进程某个事件已经发生。

a、这种通信可携带的信息极少。不适合需要经常携带数据的通信。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

b、不具备同步机制,类似于中断,什么时候产生信号,进程是不知道的。

#include <sys/sem.h>

// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

(5)共享内存

就是映射一段能被其它进程访问的内存,这段共享内存由一个进程创建,但是多个进程可以访问。

a、最快的一种通信方式,多个进程可同时访问同一片内存空间,相对其他方式来说具有更少的数据拷贝,效率较高。 

b、需要结合信号或其他方式来实现多个进程间同步,自身不具备同步机制。 

c、随内核持续,相比于随进程持续生命力更强。

(6)SOCKET通信:

a、实现起来简单,可以使用因特网域和UNIX域来实现,使用因特网域可以实现不同主机之间的进出通信。 

b、该方式自身携带同步机制,不需要额外的方式来辅助实现同步。 

c、随进程持续。

5.进程调度算法(6个),有哪些算法比较难实现?

(1)先来先服务调度算法

(2)短作业(进程)优先调度算法

(3)优先权调度算法的类型:批处理系统、实时系统中。

1) 非抢占式优先权算法:主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

2) 抢占式优先权调度算法:常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

(4)高响应比优先调度算法

(5)时间片轮转法

(6)多级反馈队列调度算法(较好)

调度算法的实施过程如下所述:

(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。

(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。

(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

6.进程阻塞状态是怎么引起的

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。

(1)阻塞原语的执行过程是:

找到将要被阻塞进程的标识号对应的PCB。

若该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行。

把该PCB插入到相应事件的等待队列中去。

当被阻塞进程所期待的事件出现时,如它所启动的I/O操作已完成或其所期待的数据已到达,则由有关进程(比如,提供数据的进程)调用唤醒原语(Wakeup),将等待该事件的进程唤醒。

(2)唤醒原语的执行过程是:

在该事件的等待队列中找到相应进程的PCB。

将其从等待队列中移出,并置其状态为就绪状态。

把该PCB插入就绪队列中,等待调度程序调度。

7.wait函数和waitpid函数

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 而子进程的进程识别码也会一起返回. 

wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。当pid=-1、option=0时,waitpid函数等同于wait。

waitpid函数提供了wait函数没有提供的三个功能:

(1)waitpid等待一个特定的进程,而wait则返回任一终止子进程的状态 。

(2)waitpid提供了一个 wait的非阻塞版本,有时希望取得一个子进程的状态, 但不想进程阻塞。

(3)waitpid支持作业控制。

8.线程、线程池使用

http://blog.51cto.com/jincheng/1763927

线程池技术把线程的创建和销毁分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段(在应用程序启动之后,就马上创建一定数量的线程,放入空闲的队列中。这些线程都是处于阻塞状态,这些线程只占一点内存,不占用CPU。

当任务到来后,线程池将选择一个空闲的线程,将任务传入此线程中运行。当所有的线程都处在处理任务的时候,线程池将自动创建一定的数量的新线程,用于处理更多的任务。执行任务完成之后线程并不退出,而是继续在线程池中等待下一次任务。当大部分线程处于阻塞状态时,线程池将自动销毁一部分的线程,回收系统资源)。

9. 特权指令与非特权指令

(1)*特权指令:在内核态运行的指令,对内存空间的访问范围基本不受限制,不仅能访问用户存储空间也能访问系统存储空间。特权指令只允许OS使用。

(2)*非特权指令:在用户态运行的指令,只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间。

10. 系统调用与过程调用

系统调用与过程调用的区别:

(1)*运行在不同系统状态:一般过程调用,其调用程序和被调用进程都运行在相同的状态(内核态或用户态);而系统调用与一般过程调用的最大区别:调用程序是运行在用户态,而被调用程序是运行在系统态。

(2)*嵌套调用:系统调用的嵌套深度有限制,而一般过程调用对嵌套无限制。

11. 用户态切换到内核态的3种方式

(1)系统调用: 用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。

(2)异常 :当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

(3)外围设备的中断: 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

12. 用户级线程和内核级线程

线程的实现可以分为两类:用户级线程(User-Level Thread)和内核级线程(Kernel-Level Thread) 

linux内核不存在整真正意义上的线程。linux将所有的执行实体都称之为任务(task),每一个任务都类似一个单线程的进程,具有内存空间、执行实体、文件资源等。但是,linux下不同任务之间可以选择公用内存空间,因而在实际意义上,共享同一个内存空间的多个任务构成了一个进程,而这些任务就成为这个任务里面的线程。

(1)用户级线程

用户级线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核以进程为单位,不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。

(2)内核支持线程

内核支持线程:由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。可以是用户进程中的线程或者系统的线程,线程可以在全系统内进行资源的竞争。

用户线程运行在一个中间系统上面。目前中间系统实现的方式有两种,即运行时系统(Runtime System)和内核控制线程。“运行时系统”实质上是用于管理和控制线程的函数集合,包括创建、撤销、线程的同步和通信的函数以及调度的函数。这些函数都驻留在用户空间作为用户线程和内核之间的接口。用户线程不能使用系统调用,而是当线程需要系统资源时,将请求传送给运行时,由后者通过相应的系统调用来获取系统资源。内核控制线程:系统在分给进程几个轻型进程(LWP),LWP可以通过系统调用来获得内核提供的服务,而进程中的用户线程可通过复用来关联到LWP,从而得到内核的服务。

(3)用户级线程和内核级线程的区别:

A. 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。

B. 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。

C. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。

D. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行,进程无法享用多处理机带来的好处;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度,内核可调度一个应用中的多个线程同时在多个处理器上并行运行,提高程序的执行速度和效率。

E. 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

F. 调度和线程执行时间:设置有内核支持线程的系统,其调度方式和算法与进程的调度十分相似,只不过调度单位是线程;对只设置了用户级线程的系统,调度的单位仍为进程。

(4)内核线程的优缺点

优点:

A. 当有多个处理机时,一个进程的多个线程可以同时执行。

B. 如果进程中的一个线程被阻塞,内核可以调度该进程的其他线程或者其他进程中的线程运行。

C. 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;内核本身也可以使用多线程的方式来实现。

缺点:

A. 由内核进行调度。

B. 对于用户的线程而言,其模式切换的开销较大,在同一进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态进行,而线程调度和管理是在内核实现的,系统开销较大。

(5)用户线程的优缺点

优点:

A. 线程的调度不需要内核直接参与,控制简单。

B.  可以在不支持线程的操作系统中实现。

C. 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。

D. 允许每个进程定制自己的调度算法,线程管理比较灵活。

E. 线程能够利用的表空间和堆栈空间比内核级线程多。

缺点:

A. 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用,多线程不能利用多处理器多重处理的优点。

B. 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。

13.线程安全

猜你喜欢

转载自blog.csdn.net/lxin_liu/article/details/89314311