1、操作系统特点

并发、共享、虚拟、异步
(1)并发:并发与并行的区别:
并行:指两个或多个事件在同一时刻发生;
并发:指两个或多个事件在同一时间间隔内发生。
具体地说:并发指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故在微观上这些程序是分时地交替执行。
若计算机系统有多个处理机,这些可以并发执行的程序便可以被分配到多个处理机上,实现并行执行。即利用每一个处理机来处理一个可并发执行的程序。
(2)共享
系统中的资源可供内存中多个并发执行的进程共同使用。 在宏观上既限定了时间(进程在内存期间),也限定了地点(内存)。
目前实现资源共享的方式(2种):
1)互斥共享方式:系统中的某些资源:如打印机、磁带机等。
2)同时访问方式:系统中还有另外一些资源,允许在一段时间内由多个进程“同时”对它们进行访问。
典型的例子:磁盘设备!
(3)虚拟:将一个物理实体 变为 若干个 逻辑上的对应物的功能。
分为“时分复用”和“空分复用”技术。时分复用 能够提高资源利用率,它利用某设备为一用户服务的空闲时间,又转去为其他用户服务,使设备得到最充分的利用。空分复用指的是将一个频率范围比较宽的信道 划分成 多个频率较窄的信道(称为频带)。
(4)异步:
异步是指,在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的,而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。

2、并发与并行

  1. 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;
  2. 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;

    3、同步异步、阻塞非阻塞

    同步:当一个同步调用发出后,调用者要一直等待返回结果。通知后,才能进行后续的执行。
    异步:当一个异步过程调用发出后,调用者不能立刻得到返回结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
    阻塞:是指调用结果返回前,当前线程会被挂起,即阻塞。
    非阻塞:是指即使调用结果没返回,也不会阻塞当前线程。
    同步是阻塞模式,异步是非阻塞模式。

    4、函数(过程)调用和系统调用的区别?

    系统调用和函数调用的区别

    系统调用

    概念
    是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。通常由高级语言编写(C或C++)。程序访问通常通过高层次 的API接口(C标准库的库函数)而不是直接进行系统调用。每个系统调用对应一个系统调用编号。
    用途

  3. 控制硬件—系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件时用到的write/read调用。

  4. 设置系统状态或读取内核数据——因为系统调用是用户空间和内核的唯一通讯手段,所以用户设置系统状态,比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过系统调用。
  5. 进程管理—系统调用接口是用来保证系统中进程能以多任务在虚拟内存环境下得以运行。比如 fork、clone、execve、exit等。

实现
Linux中实现系统调用利用了0x86体系结构中的软件中断。
首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
总结:为系统调用设置参数,系统调用号压入寄存器,产生80中断, 由用户态切换到内核态 ,保护现场将程序运行相关信息压栈,执行系统调用处理程序 system_call,根据系统调用表找和系统调用号执行完函数,返回值压入寄存器,从内核态回到用户态,恢复现场,程序获得返回值 。

函数调用

函数调用(或库函数调用,用户态, 与平台无关但是最终或多或少依赖于系统调用)
它运行在用户空间,主要通过压栈操作来进行函数调用。

系统调用与函数调用的区别
函数库调用 系统调用
在所有的ANSI C编译器版本中,C库函数是相同的 各个操作系统的系统调用是不同的
它调用函数库中的一段程序(或函数) 它调用系统内核的服务
与用户程序相联系 是操作系统的一个入口点
在用户地址空间执行 在内核地址空间执行
它的运行时间属于“用户时间” 它的运行时间属于“系统时间”
属于过程调用,调用开销较小 需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数 在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc 典型的系统调用:chdir fork write brk;

系统调用
1.使用INT和IRET指令,内核和应用程序使用的是不同的堆栈,因此存在堆栈的切换,从用户态切换到内核态,从而可以使用特权指令操控设备。
2.依赖于内核,不保证移植性
3.在用户空间和内核上下文环境间切换,开销较大。
4. 是操作系统的一个入口点
函数调用
1.使用CALL和RET指令,调用时没有堆栈切换
2.平台移植性好
3.属于过程调用,调用开销较小
4.一个普通功能函数的调用。

5、执行一个系统调用时,操作系统发生了什么?

image.png
用户运行,函数里面其实是执行的int 0x80指令。系统调用先把系统调用号保存在eax寄存器中,然后执行int0x80指令。int 0x80指令先进行切换堆栈(找到进程的堆栈,将寄存器值压入到内核栈中,将esp,ss设置成对应内核栈的值),查找相应中断向量的中断处理程序(system_call)并调用,随后system_call 从系统调用表中找到相应的系统调用进行调用,调用结束后从system_call中返回。
1.执⾏⽤户程序(如:fork) ,如库函数(系统调用的封装)
2. 根据glibc中的函数实现,取得系统调⽤号并执⾏int 0x80产⽣中断。
3. 进⾏地址空间的转换和堆栈的切换,执⾏SAVE_ALL。(进⾏内核模式)
4. 进⾏中断处理,根据系统调⽤表调⽤内核函数。
5. 执⾏内核函数。
6. 执⾏ RESTORE_ALL 并返回⽤户模式.

6、冯诺依曼结构有哪几个模块?分别对应现代计算机的哪几个部分?

核心设计思想主要体现在如下三个方面:

  • 程序、数据的最终形态都是二进制编码,程序和数据都是以二进制方式存储在存储器中的,二进制编码也是计算机能够所识别和执行的编码。(可执行二进制文件:.bin文件)
  • 程序、数据和指令序列,都是事先存在主(内)存储器中,以便于计算机在工作时能够高速地从存储器中提取指令并加以分析和执行。
  • 确定了计算机的五个基本组成部分:运算器、控制器、存储器、输入设备、输出设备

存储器:内存
控制器:南桥北桥
运算器:CPU
输入设备:键盘
输出设备:显示器、网卡

7、Windows和Linux环境下内存分布情况

image.png
用户空间内存,从低到高分别是 7 种不同的内存段:

  • 程序文件段,包括二进制可执行代码;
  • 已初始化数据段,包括静态常量;
  • 未初始化数据段,包括未初始化的静态变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关)
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了

参数,以便用户自定义大小;

8、一个由C/C++编译的程序占用的内存分为哪几个部分?

1、栈区(stack)— 地址向下增长,由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的队列,先进后出。
2、堆区(heap)— 地址向上增长,一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放 。
5、程序代码区(text)—存放函数体的二进制代码。

9、一般情况下在Linux/windows平台下栈空间的大小

Linux环境下由操作系统决定,一般是8KB,8192kbytes,通过ulimit命令查看以及修改该系统环境变量
Windows环境下由编译器决定,被记录在可执行文件中的(由编译器来设置),VC++6.0一般是1M。
linux下

$ ulimit -a# 显示当前栈的大小 (ulimit为系统命令,非编译器命令) $ ulimit -s 32768# 设置当前栈的大小为32M

VC6.0中修改堆栈大小的方法:
选择 “Project->Setting”——>选择 “Link” ——>选择 “Category”中的 “Output” ——>在 “Stack allocations”中的”Reserve:”中输栈的大小 。

10、从堆和栈上建立对象哪个快?(考察堆和栈的分配效率比较)

从两方面来考虑:

  • 分配和释放,堆在分配和释放时都要调用函数(malloc,free),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成内存碎片),这些都会花费一定的时间,具体可以看看malloc和free的源代码,函数做了很多额外的工作,而栈却不需要这些。
  • 访问时间,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正的数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。

因此从栈上建立对象更快。

11、内存可以从哪里分配?

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

12、常见内存分配内存错误

(1)内存分配未成功,却使用了它。
常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
(2)内存分配虽然成功,但是尚未初始化就引用它。
所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
(5)释放了内存却继续使用它。常见于以下有三种情况:

  • 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新

设计数据结构,从根本上解决对象管理的混乱局面。

  • 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  • 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

    13、在执行malloc申请内存的时候,操作系统是怎么做的?

    进程先通过这系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。
    这两个系统调用是brk和mmap。

  • brk是将进程数据段(.data)的最高地址指针向高处移动,这一步可以扩大进程在运行时的堆大小

  • mmap是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,这一步可获得一块可以操作的堆内存。

通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。

14、ASCII、Unicode和UTF-8编码的区别?

字符编码笔记:ASCII,Unicode 和 UTF-8
ASCII
ASCII 只有127个字符,表示英文字母的大小写、数字和一些符号,但由于其他语言用ASCII 编码表示字节不够,例如:常用中文需要两个字节,且不能和ASCII冲突,中国定制了GB2312编码格式,相同的,其他国家的语言也有属于自己的编码格式。
Unicode
Unicode就是将这些语言统一到一套编码格式中,通常两个字节表示一个字符,而ASCII是一个字节表示一个字符,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算.
Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
UTF-8
为了解决上述问题,又出现了把Unicode编码转化为“可变长编码”UTF-8编码,UTF-8编码将Unicode字符按数字大小编码为1-6个字节,英文字母被编码成一个字节,常用汉字被编码成三个字节。
UTF-8是Unicode的实现方式之一。
三者联系
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8
编码。例如,用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件。浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。

15、服务器高并发的解决方案你知道多少?

  • 应用数据与静态资源分离

将静态资源(图片,视频,js,css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器中返回静态资源,从主服务器中返回应用数据。

  • 客户端缓存

为效率最高,消耗资源最小的就是纯静态的html页面,所以可以把网站上的页面尽可能用静态的来实现,在页面过期或者有数据更新之后再将页面重新缓存。或者先生成静态页面,然后用ajax异步请求获取动态数据。

  • 集群和分布式

集群是所有的服务器都有相同的功能,请求哪台都可以,主要起分流作用。分布式是将不同的业务放到不同的服务器中,处理一个请求可能需要使用到多台服务器,起到加快请求处理的速度。
可以使用服务器集群和分布式架构,使得原本属于一个服务器的计算压力分散到多个服务器上。同时加快请求处理的速度。

  • 反向代理

在访问服务器的时候,服务器通过别的服务器获取资源或结果返回给客户端。

16、原子操作的是如何实现的?

处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
总线锁定
总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
缓存锁定
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在Pentium 6和目前的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。
“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
但是有两种情况下处理器不会使用缓存锁定。

  • 第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时, 处理器会调用总线锁定。
  • 第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。