操作系统系统面试常问(进程、线程、协程相关知识)
进程、线程和协程的区别和联系
进程定义
资源分配和调度的基本单位
线程定义
程序执行的基本单位
协程定义
用户态的轻量级线程,线程内部调度的基本单位
进程切换情况
进程切换时,操作系统会保存当前进程的CPU状态(如寄存器、页表等),并加载新进程的保存状态到CPU
线程切换情况
保存和设置程序计数器、少量寄存器和栈的内容
协程切换情况
先将寄存器上下文和栈保存,等切换回来的时候再进行恢复
线程拥有资源
CPU资源、内存资源、文件资源和句柄等
进程拥有资源
程序计数器、寄存器、栈和状态字
协程拥有资源
拥有自己的寄存器上下文和栈
线程并发性
不同进程之间切换实现并发,各自占有CPU实现并行
进程并发性
一个进程内部的多个线程并法执行
协程并发性
同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理
进程系统开销
切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大
线程系统开销
切换时只需保存和设置少量寄存器内容,因此开销很小
协程系统开销
切直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快
进程通信方面
进程间通信需要借助操作系统
线程通信方面
线程间可以直接读写进程数据段(如全局变量)来进行通信
协程通信方面
共享内存、消息队列
注意:
1、进程是资源分配的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
2、线程是资源调度的基本单位,也是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。多提一句:协程是用户态的轻量级线程,线程内部调度的基本单位
线程与进程的比较
调度
线程是调度的基本单位(PC,状态码,通用寄存器,线程栈及栈指针);进程是拥有资源的基本单位(打开文件,堆,静态区,代码段等)。
并发性
一个进程内多个线程可以并发(最好和CPU核数相等);多个进程可以并发。
拥有资源
线程不拥有系统资源,但一个进程的多个线程可以共享隶属进程的资源;进程是拥有资源的独立单位。
系统开销
线程创建销毁只需要处理PC值,状态码,通用寄存器值,线程栈及栈栈指针即可;进程创建和销毁需要重新分配及销毁task_struct结构。
一个进程可以创建多少线程,和什么有关?
这个要分不同系统去看:
如果是32位系统,用户态的虚拟空间只有3G,如果创建线程时分配的栈空间是10M,那么一个进程最多只能创建300个左右的线程。
如果是64位系统,用户态的虚拟空间大到有128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。
顺便多说一句,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响,无用线程要及时销毁。
进程线程模型你知道多少?
多线程
在用户态多线程模型中,同一个进程内的多个线程共享以下资源:
1、进程的内存空间(代码段、数据段、堆、全局变量等)
2、文件描述符等进程资源
每个线程独有的部分包括:
1、线程ID(tid)
2、独立的栈空间
3、寄存器状态(包括程序计数器PC)
关键特性:
1、对共享资源(如全局变量int i)的访问需要同步机制保证线程安全
2、线程调度顺序不可预知
3、线程切换只需保存栈、寄存器和PC,比进程切换开销小很多
注意:线程之间无法直接访问彼此的栈空间,这是保证线程独立性的关键设计。
多进程
进程是资源分配的基本单位,其结构主要包括:
1、代码段(只读,可被多个进程共享)
2、数据段(全局变量、静态变量等)
3、堆栈段(堆用于动态内存分配,栈用于函数调用)
父子进程关系:
1、fork() 后,子进程复制父进程的代码、数据和堆栈(但并非直接共享,而是读数据时复制,即修改时才真正拷贝)。
2、execv() 可让子进程加载新代码段,与父进程完全分离(如 3、shell 执行程序:fork() + execv())。
关键点:
1、父子进程初始共享数据,但写时复制保证独立性。
2、execv() 替换代码段后,子进程成为全新进程。
进程调度算法你了解多少?
先来先服务
非抢占式的调度算法,按照请求的顺序进行调度。有利于长时间,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很久时间,造成了短作业等待时间过长。
短作业优先
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长时间有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长时间永远得不到调度。
最短剩余时间优先
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间做比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
时间片轮转
将所有就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。
时间片轮转算法的效率和时间片大小有关系,如果时间片较小。则会导致切换过于频繁,在切换上就会花过多时间。反之时间片过长,则会让实时性降低。
优先级调度
为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程等不到调度,可以随着时间的推移增加等待进程的优先级
多级反馈队列
一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。多级队列是为了这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8…。进程在第一个队列没执行完,就会被转移下一个队列。这种方式下,之前的进程只需要交换7次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以看作是时间轮转调度算法和优先级调度算法结合。
Linux下进程间通信方式?
管道:
无名管道(内存文件):管道是一种半双工的通信方式,数据是能单向流动,而且只能在具有亲缘关系的进程之间使用(如fork创建的子进程)。进程的亲缘关系通常是指父子进程关系。
有名管道:也是半双工的通信方式,但是运行在没有亲缘关系的进程之间使用,管道是先进先出的通信方式。
共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量,配合使用来实现进程间的同步和通信。
//写入数据端
int main() {// 1. 生成 keykey_t key = ftok("shmfile", 65);//shmfile是文件路径,65是自己定义的0-255// 2. 创建共享内存(大小 1024 字节)int shmid = shmget(key, 1024, 0666 | IPC_CREAT);// 3. 映射共享内存char *data = (char *) shmat(shmid, NULL, 0);// 4. 写入数据strcpy(data, "你好,这是进程A写入的共享内存数据!");printf("进程A已写入数据: %s\n", data);// 5. 分离共享内存shmdt(data);return 0;
}//读取数据端
int main() {// 1. 生成 key(必须和写入端一样)key_t key = ftok("shmfile", 65);// 2. 获取共享内存int shmid = shmget(key, 1024, 0666);// 3. 映射共享内存char *data = (char *) shmat(shmid, NULL, 0);// 4. 读取数据printf("进程B读取到数据: %s\n", data);// 5. 分离 & 删除共享内存(只需要做一次)shmdt(data);shmctl(shmid, IPC_RMID, NULL); // 删除共享内存段return 0;
}
消息队列
消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。链表格式如下:
msg_pool[] —> [msg0][msg1][msg2]…[msgN]
↑ ↑
read_index write_index
信号
⽤于通知接收进程某个事件已经发⽣,⽐如按下ctrl + C就是信号。
信号量
信号量是⼀个计数器,可以⽤来控制多个进程对共享资源的访问。它常作为⼀种锁机制,实现进程、线程的对临界区的同步及互斥访问。
信号量是一个整数 + 两个基本操作:
1、P 操作(Proberen,尝试,常记为 wait()、down() 或 sem_wait())
→ 如果信号量值 > 0,执行并将值减 1;
→ 如果信号量值 = 0,阻塞等待。
2、V 操作(Verhogen,增加,常记为 signal()、up() 或 sem_post())
→ 将信号量值加 1;
→ 如果有进程/线程等待这个信号量,则唤醒一个。
套接字
与其他通信机制不同的是,它可以用于不同机器的进程通信。
Linux下同步机制
1、POSIX信号量:可⽤于进程同步,也可⽤于线程同步。
2、POSIX互斥锁 + 条件变量:只能⽤于线程同步。
怎么回收线程?有哪几种方法?
等待线程结束
int pthread_join(pthread_t tid, void** retval);
主线程调⽤,等待⼦线程退出并回收其资源,类似于进程中wait/waitpid回收僵⼫进程,调⽤pthread_join的线程会被阻塞。
tid:创建线程时通过指针得到tid值。
retval:指向返回值的指针。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>void* thread_func(void* arg) {int *ret = malloc(sizeof(int));*ret = 42; // 假设线程要返回 42return ret;
}int main() {pthread_t tid;void *retval;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, &retval);printf("Thread returned: %d\n", *(int*)retval);//也就是thread_func函数的返回值retfree(retval); // 别忘了释放线程返回的内存return 0;
}
结束线程
pthread_exit(void *retval);
⼦线程执⾏,⽤来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。
retval:同上。
分离线程
主线程、⼦线程均可调⽤。主线程中pthread_detach(tid),⼦线程中pthread_detach(pthread_self()),调⽤后和主线程分离,⼦线程结束时⾃⼰⽴即回收资源。
守护进程、僵⼫进程和孤儿进程
守护进程
守护进程是在后台运行、无控制终端的进程,通常用于周期性任务或服务,如 服务器(httpd)等。
创建守护进程要点:
1、后台运行:方法是调用fork() 创建子进程,父进程退出。
2、脱离控制终端:子进程调用 setsid(),创建新会话,脱离原有终端、会话、进程组。
3、防止重新打开终端:再次 fork(),父进程退出,子进程不再是会话组长。
4、关闭所有文件描述符:关闭从父进程继承的 0 ~ maxfd 所有文件描述符。
5、切换工作目录:chdir(“/”),避免阻止挂载文件系统卸载。
6、重置文件权限掩码:umask(0),防止继承的掩码影响新建文件权限。
7、处理 SIGCHLD 信号:设置 signal(SIGCHLD,SIG_IGN),防止产生僵尸进程。
孤⼉进程
如果⽗进程先退出,⼦进程还没退出,那么⼦进程的⽗进程将变为init进程。(注:任何⼀个进程都必须有⽗进
程)。
⼀个⽗进程退出,⽽它的⼀个或多个⼦进程还在运⾏,那么那些⼦进程将成为孤⼉进程。孤⼉进程将被init进程(进
程号为1)所收养,并由init进程对它们完成状态收集⼯作。
僵⼫进程
如果⼦进程先退出,⽗进程还没退出,那么⼦进程必须等到⽗进程捕获到了⼦进程的退出状态才真正结束,否则这
个时候⼦进程就成为僵⼫进程。
如何避免僵尸进程?
1、通过signal(SIGCHLD, SIG_IGN)通知内核对⼦进程的结束不关⼼,由内核回收。这样一来,内核会自动帮你回收子进程资源,你啥都不用管,子进程死了也不会变成僵尸。
2、⽗进程调⽤wait/waitpid等函数等待⼦进程结束,如果尚⽆⼦进程退出wait会导致⽗进程阻塞。waitpid可以
通过传递WNOHANG使⽗进程不阻塞⽴即返回。
3、注册一个 SIGCHLD 的信号处理函数,一旦子进程结束,系统自动发 SIGCHLD,你在这个函数里调用 wait() 来收回。
4、通过两次调⽤fork。⽗进程⾸先调⽤fork创建⼀个⼦进程然后waitpid等待⼦进程退出,⼦进程再fork⼀个孙
进程后退出。这样⼦进程退出后会被⽗进程等待回收,⽽对于孙⼦进程其⽗进程已经退出所以孙进程成为⼀个孤⼉进程,孤⼉进程由init进程接管,孙进程结束后,init会等待回收。
父进程、子进程、进程组、作业和会话
会话(Session)
└── 进程组(Process Group)
├── 父进程(Parent Process)
│ └── 子进程(Child Process)
└── 作业(Job)
父进程 & 子进程
1、父进程:通过 fork() 创建子进程的进程。
2、子进程:fork() 产生的新进程。
3、fork() 返回值:父进程得到子进程PID,子进程返回0。
4、两个进程代码一样,但数据空间独立,像“孪生兄弟”。
子进程继承什么?
1、会继承:用户身份、环境变量、内存内容、堆栈、进程组等。
2、不会继承:进程号不同,资源使用为0。
进程组(Process Group)
1、是一组进程的集合,有一个“组长进程”(其PID = PGID)。
2、用于统一管理多个进程(如统一接收信号)。
作业(Job)
1、shell 控制的“任务单位”,可以包含一个或多个进程组。
2、前台作业:你能直接交互的任务(shell被挂起)。
3、后台作业:在后台执行,shell可继续接受命令。
会话
会话(Session)是⼀个或多个进程组的集合。⼀个会话可以有⼀个控制终端。在xshell或者WinSCP中打开⼀个窗
⼝就是新建⼀个会话。
进程终止的几种方式
1、main函数的⾃然返回,return
2、调⽤exit 函数,属于c的函数库
3、调⽤_exit 函数,属于系统调⽤
4、调⽤abort 函数,异常程序终⽌,同时发送SIGABRT信号给调⽤进程。
5、接受能导致进程终⽌的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)
多进程和多线程的区别是什么?换句话说,什么时候该⽤多线程,什么时候该用多进程?
1、频繁修改:需要频繁创建和销毁的优先使⽤多线程
2、计算量:需要⼤量计算的优先使⽤多线程 因为需要消耗⼤量CPU资源且切换频繁,所以多线程好⼀点
3、相关性:任务间相关性⽐较强的⽤多线程,相关性⽐较弱的⽤多进程。因为线程之间的数据共享和同步⽐较简
单。
4、多分布:可能要扩展到多机分布的⽤多进程,多核分布的⽤多线程。