1 一个进程的内存管理

在操作系统中,系统会给每个进程分配虚拟地址,虚拟地址的大小与处理器的位数有关,如32位处理器进程可分配4GB的虚拟内存供程序正常运行。这4GB的虚拟内存,存储单元从地址0开始进行排序,此地址为虚拟地址。

该虚拟地址可分为五个部分:

  • 栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。线程也有自己维护的栈。
  • 堆区:程序动态申请的空间,由程序释放或其他方式释放,若没有释放,可能导致内存泄露。
  • 全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放 。
  • 文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。
  • 程序代码区:存放函数体的二进制代码。

2 DNS解析查询的详细过程


  • 首先浏览器先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。
  • 如果没找到则会查找本地DNS解析器缓存,如果查找到则返回。如果还是没有找到则会查找本地DNS服务器,如果查找到则返回。此过程是递归查询。
  • 如果没有,本地DNS服务器还要向DNS根服务器进行查询。此过程是迭代查询,按根域服务器->顶级域(.com)->第二层域(baidu.com)->子域。

3 进程的通信方式有哪些:

  • 管道:简单,只能在父子进程间单向传输,效率低下。
  • 消息队列:容量受到系统限制,发送消息(拷贝)需要花很多时间读内存,不适合频繁通信。
  • 共享内存:能够很容易控制容量,速度快,但要解决多进程竞争内存问题。
  • 信号量:信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。
  • 套接字:不仅可以用于本地进程通信,还可以用于不同主机进程之间的通信。

4 内核态和用户态:

内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

为什么要有内核态和用户态:由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据, 或者获取外围设备的数据,并发送到网络,造成安全问题。

用户态和内核态的三种切换方式:

  • 系统调用:用户进程通过系统调用申请使用操作系统提供的服务程序来完成工作,比如read()、fork()等。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现的,例如Linux的int 80h中断。
  • 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
  • 中断:当外围设备完成用户请求的操作后,会向CPU发送中断信号。这时CPU会暂停执行下一条指令(用户态)转而执行与该中断信号对应的中断处理程序(内核态)

5 孤儿进程和僵尸进程

  • 孤儿进程是父进程退出后它的子进程还在执行,这时候这些子进程就成为孤儿进程。孤儿进程会被init进程收养并完成状态收集。

  • 僵尸进程是指子进程完成并退出后父进程没有使用wait()或者waitpid()对它们进行状态收集,这些子进程的进程描述符仍然会留在系统中。这些子进程就成为僵尸进程。

6 异常和中断有何区别?

  • 中断:是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等;
  • 异常:是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)。
  • 异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。而中断则是由于系统中某事件引起的,该事件与现行指令无关。

fork() 一个父进程函数,会不会fork父进程的空间

  • 但凡是进程,都有自己的虚拟地址空间。虚拟地址空间是从0到4G的大小,其中3-4G是属于内核的。创建完子进程后,父进程继续运行app(即原来的进程)的代码,刚创建出来的子进程拥有和父进程完全一样的代码段,数据段,也就是说完完全全拷贝了一份父进程,和父进程完全一样。即clone父进程0-3G的内容,而3-4G的kernel只需要重新映射一下到物理地址的kernel即可。但是操作系统要如何区分这两个进程呢?答案就是进程ID,即pid。fork()完以后,父进程和子进程由于有着同样的数据段和代码段,栈,PCB也大部分相同,所以两个进程就会干着同样的事情,这样对我们没有意义,所以需要识别哪个是父进程,哪个是子进程,然后让父进程接着干原来的事,子进程去干新的事情。fork底层是调用了内核的函数来实现fork的功能的,即先create()先创建进程,此时进程内容为空,然后clone()复制父进程的内容到子进程中,此时子进程就诞生了,接着父进程就return返回了。而子进程诞生后,是直接运行return返回的,然后接着执行后面的程序,这里注意:子进程是不会执行前面父进程已经执行过的程序了得,因为PCB中记录了当前进程运行到哪里,而子进程又是完全拷贝过来的,所以PCB的程序计数器也是和父进程相同的,所以是从fork()后面的程序继续执行。fork()的时候,父进程的虚拟地址映射着物理内存的实际的物理地址,clone()的时候,并不是在物理地址中直接再复制一份和父进程一样的物理内存块,而是子进程的虚拟地址也直接映射到同一物理内存块中,这就是读时共享。那这样的话不是就共享变量了吗?不就和前面说的矛盾了吗? 关键:当你操作这个物理内存块时(比如修改变量的值),再复制该部分的实际物理内存到子进程中,并不是全部复制。这就是写时复制。所以,当你在后面的程序中操作遍历n时,就会另辟内存块给子进程,表示这两者的独立。这就是读时共享,写时复制。链接