操作系统面试题目详解
时间:2023-02-02 13:30:00
文章目录
-
-
-
- 1.13 什么是协程?
- 1.14 为什么协程比线程切换成本小?
- 1.15 线程和过程的区别?
- 1.16 为什么进程切换比线程消耗更多的资源?
- 1.17 介绍过程之间的通信。
- 1.18 介绍信号量。
- 1.19 谈谈僵尸和孤儿的过程。
- 1.20 请介绍过程之间的通信方式。
- 1.21 请介绍线程之间的通信方式。
- 1.22 谈谈过程的状态。
- 1.23 CPU调度的最小单位是什么?线程需要CPU调度吗?
- 1.24 共享过程中内存的通信方式有什么好处?
- 1.25 如何杀死一个过程?
- 1.26 说一说kill的原理。
- 1.27 介绍你知道的锁。
- 1.28 什么情况下会产生死锁?
- 1.29 谈谈你对自旋锁的理解。
- 1.30 谈谈你对悲观锁的理解。
- 1.31 谈谈你对乐观锁的理解。
- 1.32 CAS你在哪里用过吗?
- 1.33 谈谈IO多路复用。
- 1.34 谈谈poll和epoll的区别。
- 1.35 谈谈select和epoll的区别。
- 1.36 epoll有哪两种模式?
- 1.37 说一下epoll其查询速度为原理O(1)的吗?
- 1.38 介绍域名分析成IP的全过程。
- 1.39 如何在Linux上配置一个IP如何分析给定端口号的域名?
- 1.40 解释一下IP地址、子网掩码、网关。
- 1.41 说说IP如何寻址?
- 1.42 操作系统有几个地址,请详细说明。
- 1.43 Linux如何配置静态网络?
- 1.44 DNS使用了哪些协议?
- 1.45 说一说你对Linux了解核心。
- 1.46 说一说你对Linux了解内核态和用户态。
- 1.47 Linux什么是负载?
- 1.48 Linux如何设置启动启动?
- 1.49 谈谈Linux内存管理。
- 1.50 谈谈内存映射文件。
- 1.51 谈谈虚拟内存模型。
- 1.52 物理内存和虚拟内存是什么,为什么要有虚拟内存?
- 1.53 内存和缓存有什么区别?
- 1.54 请谈谈缓存溢出。
- 1.55 深拷贝和浅拷贝有什么区别,各自的使用场景是什么?
- 1.56 说说IO模型。
- 1.57 Linux软链接和硬链接有什么区别?
- 1.58 谈谈缺页中断机制。
- 1.59 软中断和硬中断有什么区别?
- 1.60 介绍你是对的CopyOnWrite的了解。
- 1.61 Linux如何操作替换文本?
- 1.61 Linux如何操作替换文本?
-
-
1.13 什么是协程?
参考回答
协程:协程是在子程序内执行的微线程,可以在子程序内中断,转而执行其他子程序,然后在适当的时候返回执行。
答案解析
-
线程与协程的区别:
(1)协程执行效率极高。协程直接操作栈基本没有内核切换成本,所以上下文切换非常快,切换成本比线程小。
(2)协程不需要多线程锁定机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率高于线程。
(3)一个线程可以有多个协程。
-
协程优势:
(1)协程调用和切换比线程效率高:协程执行效率极高。协程不需要多线程锁机制,可以不加锁访问全局变量,所以上下文切换非常快。
(2)协程占用的内存较少:执行协程只需要极少的栈内存(大概是4~5KB),默认情况下,线程栈的大小为1MB。
(3)切换费用较少:协程直接操作栈基本没有内核切换费用,所以切换费用比线程少。
1.14 为什么协程比线程切换成本小?
参考回答
(1)协程执行效率极高。协程直接操作栈基本没有内核切换费用,所以上下文切换非常快,切换费用小于线程。
(2)协程不需要多线程锁定机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率高于线程。避免增锁解锁费用。
1.15 线程和过程的区别?
参考回答
(1)线程从属于一个过程;一个过程可以包含多个线程。
(2)挂线程,挂相应的过程;挂一个过程不会影响其他过程。
(3)过程是系统资源调度的最小单位;线程CPU调度最小单位。
(4)进程系统费用明显大于线程费用;线程所需的系统资源较少。
(5)执行过程中有独立的内存单元和多个线程共享过程的内存,如代码段、数据段和扩展段;但每个线程都有自己的堆栈段和寄存器组。
(6)切换过程时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不同。
(8)过程适用于多核和多机分布;线程适用于多核
1.16 为什么进程切换比线程消耗更多的资源?
参考回答
需要切换过程刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;线程切换只需切换硬件上下文和内核栈。
答案解析
过程是程序的动态性能。 一个程序完成后,将使用大量的资源,如使用寄存器、内存、文件等。切换过程时,必须考虑保存当前过程的状态。状态包括存储在内存中的程序的代码和数据、堆栈、通用目的寄存器的内容、程序计数器、环境变量和件描述符的集合,称为上下文(Context)。可见,要切换过程,保存状态还是很多的。不仅如此,由于虚拟内存机制,需要进程切换刷新TLB并获取新的地址空间。
在过程中存在线程,一个过程可以有一个或多个线程。线程是运行在过程上下文中的逻辑流,这个线程可以独立完成任务。同样的线程有自己的上下文,包括唯一的整数线程ID, 栈、栈指针、程序计数器、通用目的寄存器和条件码。可以理解为线程上下文是过程上下文的子集。
由于保存线程的上下文明显小于过程的上下文,系统切换线程的成本必然较小。
1.17 介绍过程之间的通信。
参考回答
为了提高计算机系统的效率,提高计算机系统中各种硬件的并行操作能力。操作系统要求程序结构必须满足并发处理的需要。因此,引入了过程的概念。当过程平行时,需要考虑过程之间的通信。主要有匿名管道、命名管道、信号、信息队列、共享内存、信号量、Socket。
- 匿名管道:管道是一种半双工的通信方式,数据只能单向流动,只能在有亲缘关系的过程中使用。过程中的亲缘关系通常是指父子关系。
#include
#include
#include
#include
#include
int pipe_default[2];
int main()
{
pid_t pid;
char buffer[32];
memset(buffer, 0, 32);
if(pipe(pipe_default) < 0)
{
printf("Failed to create pipe!\n");
return 0;
}
if(0 == (pid = fork()))
{
close(pipe_default[1]); //关闭写端
sleep(2);
if(read(pipe_default[0], buffer, 32) > 0)
{
printf("[Client] Receive data from server: %s \n", buffer);
}
close(pipe_default[0]);
}
else
{
close(pipe_default[0]); //关闭读端
char msg[32]="== hello world ==";
if(-1 != write(pipe_default[1], msg, strlen(msg)))
{
printf("[Server] Send data to client: %s \n",msg);
}
close(pipe_default[1]);
waitpid(pid, NULL, 0);
}
return 1;
}
-
有名管道
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。
有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out) ,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。
-
信号
- 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
- 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
以下列出几个常用的信号:
信号 | 描述 |
---|---|
SIGHUP | 当用户退出终端时,由该终端开启的所有进程都退接收到这个信号,默认动作为终止进程。 |
SIGINT | 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl+C )时发出,用于通知前台进程组终止进程。 |
SIGQUIT | 和SIGINT 类似, 但由QUIT字符(通常是Ctrl+\ )来控制. 进程在因收到SIGQUIT 退出时会产生core 文件, 在这个意义上类似于一个程序错误信号。 |
SIGKILL | 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。 |
SIGTERM | 程序结束(terminate)信号, 与SIGKILL 不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。 |
SIGSTOP | 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略. |
代码示例:
下面的代码收到程序退出信号后会执行用户定义的信号处理函数来替代系统默认的处理程序。
#include
#include
#include
#include
#include
void sig_handle(int sig) {
printf("received signal: %d, quit.\n", sig);
exit(0);
}
int main () {
signal(SIGINT, sig_handle);
signal(SIGKILL, sig_handle);
signal(SIGSEGV, sig_handle);
signal(SIGTERM, sig_handle);
int i = 0;
while (1) {
printf("%d\n", ++i);
sleep(2);
}
printf("main quit.");
return 0;
}
运行结果:
1
2
received signal: 15, quit.
- 消息队列
- 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
- 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
- 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。
消息队列特点总结:
(1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
(2)消息队列允许一个或多个进程向它写入与读取消息.
(3)管道和消息队列的通信数据都是先进先出的原则。
(4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
(5)消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
(6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。
-
共享内存
进程间本身的内存是相互隔离的,而共享内存机制相当于给两个进程开辟了一块二者均可访问的内存空间,这时,两个进程便可以共享一些数据了。但是,多进程同时占用资源会带来一些意料之外的情况,这时,我们往往会采用上述的信号量来控制多个进程对共享内存空间的访问。
#include
#include #include #include #include #include using namespace std; int main() { char *shmaddr; char *shmaddread; char str[]="Hello, I am a processing. \n"; int shmid; key_t key = ftok(".",1); pid_t pid1 = fork(); if(pid1 == -1){ cout << "Fork error. " << endl; exit(1); } else if(pid1 == 0){ //子进程 shmid = shmget(key,1024,IPC_CREAT | 0600); shmaddr = (char*)shmat(shmid, NULL, 0); strcpy(shmaddr, str); cout << "[Writer] write: " << shmaddr << endl; shmdt(shmaddr); } else { //父进程 pid_t pid2 = fork(); if(pid2 == -1){ cout << "Fork error. " << endl; exit(1); } else if(pid2 == 0){ //子进程 sleep(2); shmid = shmget(key,1024,IPC_CREAT | 0600); shmaddread = (char*)shmat(shmid, NULL, 0); cout << "[Reader] read: " << shmaddread << endl; shmdt(shmaddread); } } sleep(3); return 0; } -
信号量
信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调他们的活动,这是进程之间发生的一种直接制约关系。
对信号量的操作分为P操作和V操作,P操作是将信号量的值减一,V操作是将信号量的值加一。当信号量的值小于等于0之后,再进行P操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时。锁也是用的这种原理实现的。
信号量我们需要定义信号量的数量,设定初始值,以及决定何时进行PV操作。
#include
#include #include #include #include #include #include #include #define KEY (key_t)15030110070 #define N 20 static void p(int semid ,int semNum); static void v(int semid ,int semNum); union semun { int val; struct semid_ds *buf; ushort *array; }; int main(int argc ,char* argv[]) { int i; int semid; semid = semget(KEY,3,IPC_CREAT|0660); union semun arg[3]; arg[0].val = 1; //mutex [0] 对缓冲区进行操作的互斥信号量 arg[1].val = N; //empty [1] 缓冲区空位个数n arg[2].val = 0; //full [2] 产品个数 for(i=0;i<3;i++) semctl(semid,i,SETVAL,arg[i]); pid_t p1,p2; if((p1=fork()) == 0) { //子进程1,消费者 while(1) { printf("消费者 1 等待中...\n"); sleep(2); int product = rand() % 2 + 1; for(int i = 0; i < product; i++) { p(semid ,2); //消费 p(semid ,0); //加锁 printf(" [消费者 1] 消费产品 1. 剩余: %d\n", semctl(semid, 2, GETVAL, NULL)); v(semid ,0); //开锁 v(semid ,1); //释放空位 } sleep(2); } } else { if((p2=fork()) == 0) { //子进程2,消费者 while(1) { printf("消费者 2 等待中...\n"); sleep(2); int product = rand() % 2 + 1; for(int i = 0; i < product; i++) { p(semid ,2); //消费 p(semid ,0); //加锁 printf(" [消费者 2] 消费产品 1. 剩余: %d\n", semctl(semid, 2, GETVAL, NULL)); v(semid ,0); //开锁 v(semid ,1); //释放空位 } sleep(2); } } else { //父进程,生产者 while(1) { printf("生产者开始生产...\n"); int product = rand() % 5 + 1; for(int i = 0; i < product; i++) { p(semid ,1); //占用空位 p(semid ,0); //加锁 printf(" [生产者] 生产产品 1. 剩余: %d\n", semctl(semid, 2, GETVAL, NULL) + 1); v(semid ,0); //开锁 v(semid, 2); //生产 } sleep(2); } } } return 0; } /* p操作 */ void p(int semid ,int semNum) { struct sembuf sb; sb.sem_num = semNum; sb.sem_op = -1; sb.sem_flg = SEM_UNDO; semop(semid, &sb, 1); } /* v操作 */ void v(int semid ,int semNum) { struct sembuf sb; sb.sem_num = semNum; sb.sem_op = 1; sb.sem_flg = SEM_UNDO; semop(semid, &sb, 1); } -
socket
套接字可以看做是:不同主机之间的进程进行双向通信的端点。(套接字 = IP地址 + 端口号)
1.18 介绍一下信号量。
参考回答
-
在多进程环境下,为了防止多个进程同时访问一个公共资源而出现问题,需要一种方法来协调各个进程,保证它们能够合理地使用公共资源。信号量就是这样一种机制。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
(1)函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
(2)函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
(3)函数sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) 与 sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限,如果调用因不能立即执行递减而要阻塞。
(4)函数sem_destroy(sem_t *sem)用来释放信号量sem。
-
使用示例代码如下
//g++ semtest.cpp -o test -lpthread
#include
#include
#include
#include
#include
sem_t sem;
/*function:获取当前时间,精确到毫秒 * */
int64_t getTimeMsec()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
void* func_sem_wait(void* arg)
{
printf("set wait\n");
sem_wait(&sem);
printf("sem wait success\n");
int *running = (int*)arg;
printf("func_sem_wait running\n");
printf("%d\n", *running);
}
void* func_sem_timedwait(void* arg)
{
timespec timewait;
timewait.tv_sec = getTimeMsec() / 1000 + 2;
timewait.tv_nsec = 0;
printf("sem_timedwait\n");
int ret = sem_timedwait(&sem, &timewait);
printf("sem_timedwait,ret=%d\n", ret);
printf("func_sem_timedwait running\n");
}
void* func_sem_post(void* arg)
{
printf("func_sem_post running\n");
printf("sem post\n");
int *a = (int*)arg;
*a = 6;
sem_post(&sem);
sem_post(&sem);
}
int main()
{
sem_init(&sem, 0, 0);
pthread_t thread[<