操作系统常见面试问题和解析

操作系统常见面试问题和解析

1. 进程与线程的区别和联系

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

进程和线程的关系

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

进程与线程的区别?

(1)进程有自己的独立地址空间,线程没有

(2)进程是资源分配的最小单位,线程是CPU调度的最小单位

(3)进程和线程通信方式不同(线程之间的通信比较方便。同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。)

(4)进程上下文切换开销大,线程开销小

(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程

(6)对进程进程操作一般开销都比较大,对线程开销就小了

2. 一个进程可以创建多少线程,和什么有关

(1)对于Linux来说:
linux创建一个线程会占用多少内存,这取决于分配给线程的调用栈大小,可以用ulimit -s命令来查看大小(一般常见的有10M或者是8M),一个进程的虚拟内存是4G,在Linux32位平台下,内核分走了1G,留给用户用的只有3G,于是我们可以想到,创建一个线程占有了10M内存,总共有3G内存可以使用。于是可想而知,最多可以创建差不多300个左右的线程。

进程最多可以创建的线程数是根据:

  • 分配给调用栈的大小
  • 操作系统(32位和64位不同)

(2)对于Windows来说:

  • 默认情况下,一个线程的栈要预留1M的内存空间, 而一个进程中可用的内存空间只有2G,所以理论上一个进程中最多可以开2048个线程, 但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。
  • 即使物理内存再大,一个进程中可以起的线程总要受到2GB这个内存空间的限制。 比方说你的机器装了64GB物理内存,但每个进程的内存空间还是4GB,其中用户态可用的还是2GB。

3. 一个程序从开始运行到结束的完整过程(四个过程)

(1)一个程序开始运行,首先进行创建进程,操作系统首先为该程序申请一个空白的PCB,然后向这个PCB中填入一些控制和管理进程的相关信息。然后分配所需要的资源,跳入就绪状态。

(2)程序进入就绪状态,等待处理机时间片的到来,进程被调度,获得对应的时间片,就由就绪状态跳转到运行状态。注意,时间片完了之后,进程会自动从运行状态跳到就绪状态,等待下一个时间片的到来。

(3)如果程序运行过程中请求某一个资源,例如IO资源,这个时候IO资源正在忙碌,此时程序主动进入阻塞状态,等待IO资源的空闲。

(4)当IO资源空闲,会主动由另外一个进程唤醒正在阻塞的进程,这个时候进程转为就绪状态,等待时间片的到来。

(5)运行完成之后,进行结束状态,操作系统回收一些资源的工作。

4. 进程通信方法(Linux和windows下),线程通信方法(Linux和windows下)

(1)linux下进程的通信方法:1.管道 2.信号量 3.共享内存 4.消息队列 5.套接字

  • 管道

    管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。

    注1:无名管道只能实现父子或者兄弟进程之间的通信,有名管道(FIFO)可以实现互不相关的两个进程之间的通信。

    注2:用FIFO让一个服务器和多个客户端进行交流时候,每个客户在向服务器发送信息前建立自己的读管道,或者让服务器在得到数据后再建立管道。使用客户的进程号(pid)作为管道名是一种常用的方法。客户可以先把自己的进程号告诉服务器,然后再到那个以自己进程号命名的管道中读取回复。

  • 信号量

    信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 共享内存:

    共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。

  • 消息队列

    是一个在系统内核中用来保存消 息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 套接字

    套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

(2)windows下进程通信的方法:1.文件映射 2. 共享内存(是文件映射的一种特殊情况);3.邮件槽(mailslot)(点对点消息队列); 4.匿名管道;5;命名管道; 6.socket;

  • 文件映射

    文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,只需简单的指针操作就可读取和修改文件的内容。

    Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。

  • 共享内存

    Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映 射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能 运行于同一计算机上的进程之间。

  • 匿名管道

    管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。

  • 命名管道

    命 名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器 建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。

    命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。

  • 邮件槽

    邮件槽(Mailslots)提供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。

    邮 件槽与命名管道相似,不过它传输数据是通过不可靠的数据报(如TCP/IP协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,而命名 管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,所以邮件槽不失为应用程序发送和接 收消息的另一种选择。

  • Sockets

    Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。

    ——linux下线程通信:线程之间是整个地址空间的资源都是共享的,所以只能做好同步互斥即可,在线程中同步互斥用到的工具有:锁机制,信号机制等

    ——windows线程通信:1.全局变量,2,message消息队列机制

    http://blog.youkuaiyun.com/richerg85/article/details/7655840

5.文件读写使用的系统调用

文件读写会涉及到的系统调用:

open:打开某个文件,并配置响应的权限

close:关闭文件描述符

read:读取函数,设置缓存区,及缓存区的大小

write:写操作函数,指定写入数据的大小

lseek:指定文件偏移量函数,文件偏移量指的是当前文件操作位置相对于文件开始位置的偏移。

fstat:获取文件状态

mmap

  • 建立内存映射函数,mmap()函数将普通文件映射到内存中,普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。
  • mmap()映射后,让用户程序直接访问设备内存,相比较在用户控件和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。

**fcntl:**文件属性的调用

**ioctl: ioctl()**函数通过对文件描述符的发送命令来控制设备。

(1)读文件

  • 进程调用库函数向内核发起读文件请求;

  • 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项;

  • 调用该文件可用的系统调用函数read()

  • read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode;

  • 在inode中,通过文件内容偏移量计算出要读取的页;

  • 通过inode找到文件对应的address_space;

  • 在address_space中访问该文件的页缓存树,查找对应的页缓存结点:

    1)如果页缓存命中,那么直接返回文件内容;

    2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;

  • 文件内容读取成功。

(2)写文件

  • 前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:

  • 如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。

  • 如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。

  • 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
    1)手动调用sync()或者fsync()系统调用把脏页写回

    2)pdflush进程会定时把脏页写回到磁盘

同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。

参考:
https://www.cnblogs.com/huxiao-tee/p/4657851.html

6. 怎么回收线程

  • 线程退出有多种方式,如return,pthread_exit,pthread_cancel等;
  • 线程分为可结合的(joinable)和分离的(detached)两种,如果没有在创建线程时设置线程的属性为PTHREAD_CREATE_DETACHED,则线程默认是可结合的。
  • 可结合的线程在线程退出后不会立即释放资源,必须要调用pthread_join来显式的结束线程。
  • 分离的线程在线程退出时系统会自动回收资源。

7. 守护进程、僵尸进程和孤儿进程

  • 守护进程:守护进程就是在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或其他特殊用户运行,并能处理一些系统级的任务。
  • 孤儿进程:父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  • 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

8. 处理僵尸进程的两种经典方法

方法一:父进程回收法
wait函数将使其调用者阻塞,直到其某个子进程终止。故父进程可调用wait函数回收其僵尸子进程。除此之外,waitpid函数提供更为详尽的功能( 增加了非阻塞功能以及指定等待功能 ),请读者自行查阅相关资料。
代码实现

 1 #include <unistd.h>
 2 #include <sys/wait.h>
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 
 6 int main()
 7 {
   
   
 8     int pid;
 9     int *status;
10 
11     printf("%s\n", "启动父进程");
12 
13     if ((pid = fork()) < 0) {
   
   
14         printf("%s\n", "创建子进程失败");
15         exit(1);
16     }
17     else 
18         if (pid ==0) {
   
   
19             printf("%s\n", "进入子进程");
20             sleep(4);
21             // 终止子进程
22             exit(0);
23         }
24     else {
   
   
25         // 进入父进程
26         // 回收僵尸子子进程
27         wait(status);
28         printf("%s\n", "回收完毕");
29     }
30 
31     exit(0);
32 }

结果分析
第三行的“回收完毕”是在程序执行四秒后才显示的。这说明尽管我将子进程阻塞了4秒,父进程并不会先于子进程终止。因为它调用了wait函数,故需要等待一个子进程结束并将其回收,否则就一直阻塞在那里。

方法二:init进程回收法

  • 上面的这种解决方案需要父进程去等待子进程,但在很多情况下,这并不合适,因为父进程也许还有其他任务要做,不能阻塞在这里。在讲述下面这种不用父进程等待就能完成回收子进程的方法之前,先请明白以下两个概念:
    1)如果父进程先于子进程结束,那么子进程的父进程自动改为 init 进程。

    2)如果 init 的子进程结束,则 init 进程会自动回收其子进程的资源而不是让它变成僵尸进程。

代码实现

 1 #include "apue.h"
 2 #include <sys/wait.h>
 3 
 4 int
 5 main(void)
 6 {
   
   
 7     pid_t    pid;
 8 
 9     if ((pid = fork()) < 0) {
   
       // 创建第一个子进程
10         err_sys("fork error");
11     } else if</
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值