面试题之操作系统
进程是资源分配的最小单位,线程是程序执行(CPU调度)的最小单位(资源调度的最小单位)。一个线程只能属于一个进程,而一个进程可以有多个线程,至少有一个线程。
并行和并发
并发就是在一段时间内,多个任务都会被处理 但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程A和B,A运行一个时间片之后,切换到B,B运行一个时间片之后又切换到A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。
并行就是在同一时刻,有多个任务在执行。 这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。
进程和线程区别
线程的优缺点
为什么有了进程还需要引入线程?
应用程序的需要:某些应用程序需要同时发生多种活动,比如字符处理软件,当输入文字时,排版也在同时进行,自动保存也在进行。如果用线程来描述这样的活动的话,编程模型就会变得更简单,因为同一进程的所有线程都处于同一地址空间,拥有相同的资源。
开销上考虑:线程更加轻量级,相对进程而言,线程的相关信息较少,它更容易创建,也更容易撤销。当有大量线程需要创建和修改时,这会节省大量的开销;线程之间的切换比进程之间的切换要快的多,因为切换线程不需要考虑地址空间,只需要保存维护程序计数器,寄存器,堆栈等少量信息;线程之间的通信也比进程之间的通信要简单,无需调用内核,直接通过共享变量即可。
#include
pid_ t fork (void) ; // 子进程创建
void exit(int status) ; // 进程退出。需要 include
void _exit(); // 在头文件 中声明
/* Get the process ID of the calling process. */
extern __pid_t getpid (void) __THROW; // 获得当前进程的 Id
/* Get the process ID of the calling process's parent. */
extern __pid_t getppid (void) __THROW; // 获得父进程的 ID
/* Get the process group ID of the calling process. */
extern __pid_t getpgrp (void) __THROW; // 获得进程组的 ID
/* Return the foreground process group ID of FD. */
extern __pid_t tcgetpgrp (int __fd) __THROW; //返回前台进程组 ID, 这与在 __fd打开的终端相关
Note exit()函数与_exit()函数最大区别就在于exit()函数在调用exit 系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。
三种基本状态:ready(就绪),running(运行),wait(等待).
其它状态:创建(new)、终止(terminated)、挂起(suspend)
fork():通过复制调用进程来建立新的进程,是最基本的进程建立操作,对父进程返回子进程的pid,对子进程返回0。
exec():包括一系列的系统调用,通过用一段新的程序代码,覆盖原来的空间,实现进程执行代码的转换。
wait():提供初级进程同步操作,使一个进程等待另外一个进程的结束。
exit():终止一个进程的运行
当有任务需要处理,但是CPU资源有限,这些任务无法同事处理,就要按照某些规则来决定处理顺序,这就是调度。常见的调度算法有:先来先服务、短作业优先、最短剩余时间优先、时间片轮转、优先级调度。
临界区、互斥量、信号量、管程
套字节主要用于不同机器之间的网络通信,其它几种用于同一机器上进程之间的通信
管道是一种两个进程间进行单向通信的机制。
特点:(1)数据只能由一个进程流向另一个进程(其中一个读管道, 一个写管道);如果要进行双工通信,则需要建立两个管道。
(2) 管道只能用于父子进程或者兄弟进程间通信,也就是说管道只能用于具有亲缘关系的进程间通信。
#include
int pipe(int fd[2]); // 管道创建
// fd[O]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入
int msgget(key_t key, int msgflg) ; // 创建消息队列
/** 从队列中取用消息 **/
ssize_t msgrcv(int msqid , void *msgp, size_t msgsz, long msgtyp, int msgflg);
/** 将数据放到消息队列中 **/
int msgsnd(int msqid ,const void *msgp, size_t msgsz, int msgflg);
/** 设置消息队列属性 **/
int msgctl(int msgqid, int cmd, struct msqid_ds *buf);
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是最快的一种IPC
#include
/** 创建共享内存 **/
int shmget(key_t key, int size, int flag) ;
/** 其它进程调用 shmat 将其连接到自身的地址空间中 **/
void *shmat(int shmid, void *addr, int flag) ;
/** 将共享内存从当前进程中分离 **/
int shmdt(const void *shmaddr) ;
共享内存的优缺点:
#include
#include
#include
/** 创建和打开信号量 **/
int semget(key_t key, int nsems, int semflg) ;
/** 用于改变信号量的值 **/
int semop(int semid, struct sembuf *sops, unsigned nsops) ;
/** 用于直接控制信号量信息 **/
int semctl(int semid, int semnum, int cmd, ... /* union semum arg */) ;
网络中的进程是通过套字节来通信的,不论他们是在同一计算机还是不同计算机之间。
以TCP协议通信的socket其交互过程大概如图:
一种在windows平台上的网络编程实现:【TCP】网络编程学习
僵尸、孤儿、守护进程
当子进程比父进程先结束,而父进程又没有回收子进程(调用wait/waitpid),释放子进程占用的资源,此时子进程将成为一个僵尸进程。会造成内存泄漏。
僵尸进程如何避免?
1、让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或者waitpid(),通知内核释放僵尸进程 。
2、采用信号SIGCHLD通知处理,并在信号处理程序中调用wait函数 。
3、让僵尸进程变成孤儿进程,就是让他的父亲先死。
4、如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
孤儿进程就是说一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init
进程(进程ID
为1
的进程)所收养,并由 init
进程对它们完成状态收集工作。因为孤儿进程会被 init
进程收养,所以孤儿进程不会对系统造成危害。
孤儿进程和僵尸进程的区别:孤儿进程是父进程已退出,而子进程未退出;僵尸进程是父进程未退出,而子进程已退出。
运行在后台的一种特殊进程,它是独立于控制终端的,并周期性地执行某些任务。
/* Create a new thread, starting with execution of START-ROUTINE
getting passed ARG. Creation attributed come from ATTR. The new
handle is stored in *NEWTHREAD. */
extern int pthread_create (pthread_t *__restrict __newthread,
const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg); // 创建新增线程。如果创建成功,返回0,否则返回错误编号
/* Obtain the identifier of the current thread. */
extern pthread_t pthread_self (void); // 获取自身线程 ID
/* Terminate calling thread.*/
extern void pthread_exit (void *__retval); // 结束线程,一般子线程调用
/*This function is a cancellation point and therefore not marked with __THROW.*/
extern int pthread_join (pthread_t __th, void **__thread_return); // 结束线程,一般主线程调用
锁机制:包括互斥锁/量(mutex)、读写锁(reader-writer lock)、自旋锁(spin lock)、条件变量(condition)
条件变量
信号量机制(Semaphore)
信号机制(Signal)
屏障(barrier)
取保同一时间只有一个线程访问数据。从本质上说是一把锁,在访问共享资源之前进行加锁。访问完成之后释放(解锁)。
pthread_ mutex _lock()//加锁
... //共享的资源的操作
pthread_ mutex _unlock()
读写锁有三种状态:读模式下的加锁状态、写模式下的加锁状态和不加锁状态。
一 次 只 有 一 个 线 程 可 以 占 有 写 模 式 的 读 写 锁 , 但 是 多 个 线 程 可 以 同 时 占 有 读 模 式 的 读 写 锁 。 写 操 作 是 排 它 性 的 , 独 占 的 , 读 操 作 是 共 享 的 , 允 许 多 个 线 程 同 时 去 访 问 同 一 资 源 \textcolor{red}{一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。写操作是排它性的,独占的,读操作是共享的,允许多个线程同时去访问同一资源} 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。写操作是排它性的,独占的,读操作是共享的,允许多个线程同时去访问同一资源
与互斥量相比,读写锁允许更高的并行性。互斥锁只有两个状态(加锁状态、不加锁状态)。读写锁三种状态。
读写锁的三种状态:
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。
当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求则长期阻塞。
通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。
使用过程:创建 --> 注销 —> 等待 —> 激发
// 创建
pthread_cond_t cond=PTHREAD_COND_INITIALIZER; // 静态方式创建
int pthread_cond_init(pthread_cond_t *cond , pthread_condattr_t *cond_attr) // 动态方式创建
// int pthread_cond_destroy(pthread_cond_t *cond) // 注销
pthread_cond_wait(); // 等待
pthread_cond_signal(); // 激发
在两个或多个并发进程中,如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么该进程集合就产生了死锁。
死锁产生的根本原因是多个进程竞争资源时,进程的推进顺序出现不正确。
死锁产生的必要条件
互斥条件(Mutual exclusion ):资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait ):已经得到资源的进程可以再次申请新的资源。
非剥夺条件(No pre-emption ):已经分配的资源不能从相应的进程中被强制地剥夺。
循环等待条件(Circular wait ):系统中若干进程组成环路,改环路中每个进程都在等待相邻进程正占用的资源。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
处理死锁的基本方法
鸵鸟策略(即直接忽略死锁)、死锁预防、死锁避免、死锁检测和恢复
参考