操作系统 Flashcards

1
Q

什么是僵尸进程,什么是孤儿进程

A

在正常情况下,一个父进程创建了一个子进程,当这个子进程调用exit或者运行时发生致命错误或者收到了终止信号,它就会终止,然后它的返回值会回报给操作系统,然后操作系统会用SIGCHILD信号通知父进程子进程已经终止了,父进程通过wait或者waitpid获取子进程的终止状态,然后系统内核从内存中释放已结束的子进程的PCB

上面是正常情况下子进程退出时的流程,但是有的时候会出现两个问题,一个是孤儿进程,一个是僵尸进程

孤儿进程就是父进程先退出,但是它还有一个或者多个子进程还在运行,那么这些子进程就成了孤儿进程,孤儿进程将被init进程,也就是进程号为1的进程收养,然后当它们退出的时候退出状态会由init进程收集,所以孤儿进程并不会造成太大的危害

僵尸进程就是当子进程退出后,操作系统用SIGCHILD信号通知父进程,此时子进程虽然已经结束但是它的进程控制块也就是PCB还留在内存中,父进程收到了SIGCHILD信号却没有使用wait或者waitpid来获取子进程的退出状态,就导致操作系统一直没有把子进程的PCB从内存中释放掉,此时子进程的状态通过ps -aux查看发现是Z,就是zombie,也就是僵尸进程了。僵尸进程是有危害的,因为父进程没有获取退出信息,它的PCB所占用的空间也就不会被释放,因为我们知道PCB是个结构体会占用一些空间,这就造成了资源浪费

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

如何解决僵尸进程的问题

A

主要分为两种方式,清理僵尸进程和避免僵尸进程

清理僵尸进程不能直接用kill,用kill -9也杀不掉,但是可以用kill杀死僵尸进程的父进程,这样一来僵尸进程就会变成孤儿进程,由init进程收养,init进程就会调用wait(),然后就会将其释放。还有就是重启服务器

避免僵尸进程,也是由两种方法,一种是修改SIGCHILD信号的信号处理函数,可以在信号处理函数中调用wait获取进程结束状态,也可以用signal函数把SIGCHILD信号的处理方式改成SIG_IGN,这样就向系统申明了不关注子进程的退出状态,子进程退出后系统就不等待父进程操作而是直接回收这个子进程的资源。还有一种办法就是父进程创建子进程的时候连续调用两次fork(),并且让第一代子进程直接退出,并且用waitpid()给它收尸,这样第二代子进程的父进程就直接变成了init进程,它退出的时候就由init进程处理,这样也就不会出现僵尸进程了

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

什么是大端字节序,什么是小端字节序

A

在计算机系统中,以字节为存储单元,每个地址单元对应一个字节,一个字节是八个比特位,而在编程语言中,不仅用一个字节来存储一个数据,除了一个字节的char还有short、int等等多于一个字节的数据类型,这样的话,就出现了多个字节如何排布的问题,于是就有两种排布方式,一种是大端字节序,一种是小端字节序

大端存储模式,就是指数据的低位保存在内存的高地址中,小端存储模式,是指数据的低位保存在内存的低地址中

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

如何测试电脑是大端模式还是小端模式

A

两种方法:

第一种,定义一个int变量值等于1,然后用一个int类型的指针指向它,再强转成char类型,然后解引用输出,如果是1就代表低位存在低地址,所以是小端序,如果是0代表高位存在低地址,所以是大端序

第二种,定义一个联合体,一个int成员,一个char成员,给int成员赋个1,然后输出char成员的值,如果是0代表高位存在低地址,就是大端序,是1表示低位存在低地址,就是小端序

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

vim如何查找一个字符串

A

普通模式下,按斜杠键,输入想要查找的字符串,按回车,就可以向下查找字符串,按问号键,输入想查找的字符串,再按回车,就是向上查找字符串,然后按小写n是向下找下一个,按大写N是向上找下一个

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

讲一下vim替换操作

A

普通模式按冒号键进入命令模式,然后输入s斜杠斜杠斜杠g,前两个斜杠中间填要被替换的字符串,后两个斜杠中间填要替换成的字符串,这就是替换当前行的所有匹配到的字符串,去掉g就只替换当前行的第一个,还可以在s前面加上行号逗号行号,就是替换这两个行号之间的匹配到的字符,或者在s前面加百分号,就是替换全文匹配到的字符

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

讲一下gdb和调试的一般过程和指令

A

gdb,全称GNU Debugger,是unix环境下的调试工具,它可以用于调试多种语言的程序,但是一般多用于调试C和C++的程序。

一般在使用gdb进行调试的时候,首先要在使用gcc进行编译的时候加上-g参数,这样会保留调试信息,如果不加-g参数,会发现gdb命令也可以运行,但是只能通过run指令跑完程序,查看代码、变量、打断点这些功能都不能正常执行了,这是因为gcc编译不加-g的时候默认编译出的文件是realese版本,也就是发行版,不带调试信息。编译完成之后就可以通过gdb加文件名的命令开始调试,调试的主要命令有:list-显示10行源代码,break-设置断点,run-运行程序,next-单步运行不进入函数,step-单步运行进入函数,print-打印变量的值,continue-继续运行直到下一个断点,等等。上面说的这些指令都可以简写成首字母。还有一个常用的是where可以看到函数的调用栈。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

GDB调试core文件

A

程序在一些情况下发生崩溃的时候会发生coredump,产生一个core文件,里面包含了程序运行时的内存、寄存器、堆栈、函数调用等信息,我们可以使用GDB去分析core文件,从而找到程序异常退出的问题所在。core文件一般默认是保存在和可执行程序的同一目录,但是在代码中可以对core文件保存的目录进行修改。但是首先,要通过ulimit -c命令查看当前环境是否会生成core文件,如果发现当前环境限制了core文件的大小为0,还要用ulimit -c unlimited命令更改设置才能生成core文件。设置好了之后如果程序运行出现野指针、内存越界、多线程可重入函数出错、多线程数据未加锁,这些段错误的话,就会提示segmentation falut(core dumped),这就发生了核心转储。之后就可以用gdb对core文件进行调试,主要就是用bt(backtrace)或者where查看函数调用栈,就可以知道是在哪个函数中发生的错误,然后就可以定位到问题的根源,然后再去看具体函数的源代码,查找并解决问题

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

GDB 调试已运行程序

A

首先使用 ps -ef | grep 进程名 命令找到进程id,然后命令行输入gdb,进入gdb命令行,然后使用attach加进程id,然后就可以调试运行中的程序了

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

调试多线程

A

调试多线程的话,基础步骤和命令跟调试单线程程序一样,大概就是编译的时候加上-g,然后就用gdb加可执行文件名命令进入gdb调试,然后多线程调试的主要命令有info threads–显示当前所有线程,每个线程会有一个gdb分配的ID,thread + 线程ID–就可以切换当前调试的线程为指定ID的线程,thread apply + 线程ID号 + 命令–让多个线程执行命令,还有就是set scheduler-locking +off/on/step,就是设置单步执行调试的时候别的线程是否会同时执行,off就是所有线程都运行,on就是锁定,只有当前线程运行,step就是单步的时候锁定,只有当前线程可以执行,非单步的调试不锁定,都可以执行

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

编译链接的过程

A

一个C++源文件,从文本到可执行文件一般需要四个过程,分别是预处理、编译、汇编、链接,如果有一个叫做main.cpp的源文件,它首先经过预处理器会翻译成一个main.i的中间文件,之后编译器会将它翻译成一个汇编语言文件main.s,之后汇编器将它翻译成一个可重定位目标文件main.o,再之后链接器将它和用户生成的其它可重定位目标文件还有系统目标文件组合起来,就形成了一个可执行目标文件

具体来说,

预处理阶段:
主要处理源代码中”#”开头的指令,比如#include、#define、#if、#endif等等,除了#pragma的指令是留给编译阶段处理的,还会删除代码中的注释等等

编译阶段:
对预处理后生成的.i文件进行词法、语法、语义的优化,优化之后生成相应的汇编代码

汇编阶段:
将汇编代码翻译成机器码,生成一个可重定位目标文件

链接阶段:
把所有可重定位目标文件进行分段的合并,其中符号表合并后,进行符号解析,然后将符号进行重定位,比如说我main.cpp里有个sum函数的声明,这个函数在sum.cpp里实现,那么sum函数在main.cpp里也会产生符号,但是找不到它的地址,当进行了上述过程之后,就只剩一个带有地址的符号了。最终链接器会把多个目标文件和静态库连接成最终的可执行目标文件

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

静态链接库和动态链接库

A

静态库

静态库就是静态链接库,在Windows下后缀是.lib,在Linux下后缀是.a,静态链接在源代码生成可执行程序的四个阶段中的链接阶段进行,它会把库文件中所有的内容加入到可执行文件中

两个缺点,一个优点:

这样就会导致两个问题,一个是空间浪费,因为每当编译一个静态库就把该静态库复制了一遍,当多个可执行程序用同一个静态库时可能会导致内存里有许多相同的代码复制了很多份;还有一个问题就是如果我们更改了静态库里的一点点代码,也需要重新进行一遍编译链接的过程,许多大型工程编译一遍需要很长时间,这样效率不高

但是静态链接库也有优点,就是可执行文件包含了库里的内容,在执行可执行文件时就不需要加载了,运行速度会比较快

动态库

动态库就是动态链接库,在Windows下后缀时.dll,Linux下是.so,与静态库相反,动态库在编译时并不会被拷贝到可执行文件里,可执行文件里只会存储指向动态库的引用,等到程序运行时才会把动态库加载进来

两个优点,一个缺点:

因为不会拷贝到程序里,也就不会影响程序的提及,节省了空间,而且可以同一份库被多个程序使用,所以动态库也可以叫做共享库。而且由于动态库在程序运行时才会被载入,所以进行对库的更新或者替换操作的时候不需要把整个程序都重新编译链接一遍,但是也会带来一个问题就是动态库把链接推迟到了程序运行的时候,所以每次执行程序都要进行链接,性能上会有损失

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

什么是进程

A

首先,通俗的说,进程就是运行中的程序。在计算机中,操作系统管理着所有的软硬件资源,一般操作系统会通过一种“先描述,再组织”的方式对资源进行管理,也就是说操作系统首先要把进程这个抽象的概念用计算机的方式描述出来,然后才可以对这些不再抽象了的信息进行组织。而操作系统对进程的描述,就是PCB(process control block),在Linux下,PCB是一个名叫task_struct的结构体。在这个结构体中,保存了一些例如标识符、记录进程状态的信息、记录进程即将执行的下一条指令的信息等等这些信息。通过PCB和内存中进程对应的代码与数据,就真正的组成了一个进程的实体。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

如何查看一个进程

A

第一种方式,可以在/proc系统文件夹查看,在这个文件夹内可以通过pid找到对应的进程

第二种方式,通过ps命令查看,常用的参数有ps -aux和ps -ef,ps -aux显示的内容比较全,会包括进程状态、进程占用的内存这些,但是ps -ef能显示进程的父进程的pid。一般在使用的时候我们会在ps命令后面加上个管道,grep加进程pid或者进程名来筛选出要找的进程

第三种方式,可以用top命令动态地显示系统当前所有进程

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

什么是守护进程

A

守护进程,英文是deamon,又叫精灵进程,这种进程的名称一般用字母d结尾,它在后台运行,不受终端控制,终端退出的时候它不会退出,当服务器关闭的时候它才会退出,所以一般网络服务会以守护进程的方式运行,它也是一个特殊的孤儿进程,它的父进程是init进程

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

进程虚拟地址空间的区域划分

A

在x86体系的32位Linux环境下,操作系统会给当前的每一个进程分配一个2的32次方大小也就是4G的空间,这块空间叫做进程的虚拟地址空间,这块空间又分为一个1G的内核空间和一个3G的用户空间。每一个进程的用户空间是私有的,但是内核空间是所有进程所共享的。我们从低地址向高地址分别说一下每块空间的作用,首先从0x00000000开始的一小段空间是预留的,不能访问,然后上面是只读段,包含了.init、.text、.rodata这些内容,它们存储了程序编译生成的指令和一些只读的数据。然后往上是读写段,分为.data段和.bss段,.data段存放初始化了的且初始化不为0的数据,.bss段存放未初始化或者初始化为0的数据,当程序运行时操作系统会将.bss段的数据全部赋值为0,所以打印一个未初始化的全局变量可以看到值为0。再往上就是堆,在《深入理解计算机系统》中的名称是运行时堆(由malloc创建),也就是说这里的堆空间在程序刚开始运行的时候是暂时没有的,只有当运行到malloc的指令的时候才会分配堆内存,并且使由低地址向高地址增长的。再往上是动态链接库的映射区域,再往上就是栈空间,每一个函数或者线程独有,由高地址向低地址增长。最后上面的空间存放命令行参数和环境变量。再往上就是所有进程共享的内核空间了

17
Q

进程虚拟地址空间的区域划分

A

在x86体系的32位Linux环境下,操作系统会给当前的每一个进程分配一个2的32次方大小也就是4G的空间,这块空间叫做进程的虚拟地址空间,这块空间又分为一个1G的内核空间和一个3G的用户空间。每一个进程的用户空间是私有的,但是内核空间是所有进程所共享的。我们从低地址向高地址分别说一下每块空间的作用,首先从0x00000000开始的一小段空间是预留的,不能访问,然后上面是只读段,包含了.init、.text、.rodata这些内容,它们存储了程序编译生成的指令和一些只读的数据。然后往上是读写段,分为.data段和.bss段,.data段存放初始化了的且初始化不为0的数据,.bss段存放未初始化或者初始化为0的数据,当程序运行时操作系统会将.bss段的数据全部赋值为0,所以打印一个未初始化的全局变量可以看到值为0。再往上就是堆,在《深入理解计算机系统》中的名称是运行时堆(由malloc创建),也就是说这里的堆空间在程序刚开始运行的时候是暂时没有的,只有当运行到malloc的指令的时候才会分配堆内存,并且使由低地址向高地址增长的。再往上是动态链接库的映射区域,再往上就是栈空间,每一个函数或者线程独有,由高地址向低地址增长。最后上面的空间存放命令行参数和环境变量。再往上就是所有进程共享的内核空间了

18
Q

进程间通信的方式

A

有的时候想要在两个进程之间实现数据传输、资源共享、通知事件、还有进程控制,就要实现两个进程之间的通信,因为我们知道进程之间是相互独立的,有各自的虚拟地址空间,并且虚拟地址和真实物理内存之间还有复杂的映射关系,所以进程间通信是很难的。所以操作系统提供了一份公共资源,让两个进程都能访问,这样就实现了通信,由此也就引出了六大进程间通信的方式

第一种就是管道,管道分为匿名管道和命名管道,顾名思义就是用这个东西实现数据的流通,但是管道都是半双工的,也就是数据可以从A端流向B端,也可以从B端流向A端,但是在同一时刻只能出现一种流向,当然如果想实现全双工通信用两根管道就可以了。当然在Linux下一切皆文件所以其实管道就是在内核中的一份文件,进程A能看到进程B也能看到,进程A写数据,进程B读数据。至于匿名管道和命名管道的区别就是匿名管道只能用于有亲缘关系的进程间通信而命名管道则是用于任意进程

第二种是消息队列,消息队列其实就是一个内核中的链表,链表节点中存放着数据报的类型和内容,每个消息节点的最大长度和消息队列的总字节数还有系统中消息队列的总数都是有上限的。进程A可以像队列中写数据,队列中有了数据之后进程B就可以读数据,它同时还能写数据,所以消息队列是全双工的

第三种是共享内存,共享内存就是将同一块物理内存映射到不同的进程的虚拟地址空间中,就可以让多个进程都能访问同一块内存,对这块内存进行读写操作,共享内存的速度相较于其它通信方式是比较快的

第四种是信号量,信号量的本质是内核中的一个计数器和一个pcb等待队列,它可以通过对临界资源进行计数从而实现进程间的同步与互斥,互斥就是保证同一时间只有一个进程访问临界资源,同步就是通过对是否存在临界资源进行判断,没有资源就等待,有资源就唤醒一个进程

第五种是信号,信号就是一条消息,它通知进程发生了一个某种类型的事件,信号的实现稍微复杂一点,大概就是通过一个pcb中的pending信号集合,就是未决信号集合,本质上是一个位图,用来标记信号信息,信号产生之后会在位图中经历注册、注销、捕获处理的过程最终调用相应的信号处理函数来处理信号

第六种是SOCKET套接字,这种进程间通信机制可以用于不同主机之间的进程通信

19
Q

进程间通信的方式

A

有的时候想要在两个进程之间实现数据传输、资源共享、通知事件、还有进程控制,就要实现两个进程之间的通信,因为我们知道进程之间是相互独立的,有各自的虚拟地址空间,并且虚拟地址和真实物理内存之间还有复杂的映射关系,所以进程间通信是很难的。所以操作系统提供了一份公共资源,让两个进程都能访问,这样就实现了通信,由此也就引出了六大进程间通信的方式

第一种就是管道,管道分为匿名管道和命名管道,顾名思义就是用这个东西实现数据的流通,但是管道都是半双工的,也就是数据可以从A端流向B端,也可以从B端流向A端,但是在同一时刻只能出现一种流向,当然如果想实现全双工通信用两根管道就可以了。当然在Linux下一切皆文件所以其实管道就是在内核中的一份文件,进程A能看到进程B也能看到,进程A写数据,进程B读数据。至于匿名管道和命名管道的区别就是匿名管道只能用于有亲缘关系的进程间通信而命名管道则是用于任意进程

第二种是消息队列,消息队列其实就是一个内核中的链表,链表节点中存放着数据报的类型和内容,每个消息节点的最大长度和消息队列的总字节数还有系统中消息队列的总数都是有上限的。进程A可以像队列中写数据,队列中有了数据之后进程B就可以读数据,它同时还能写数据,所以消息队列是全双工的

第三种是共享内存,共享内存就是将同一块物理内存映射到不同的进程的虚拟地址空间中,就可以让多个进程都能访问同一块内存,对这块内存进行读写操作,共享内存的速度相较于其它通信方式是比较快的

第四种是信号量,信号量的本质是内核中的一个计数器和一个pcb等待队列,它可以通过对临界资源进行计数从而实现进程间的同步与互斥,互斥就是保证同一时间只有一个进程访问临界资源,同步就是通过对是否存在临界资源进行判断,没有资源就等待,有资源就唤醒一个进程

第五种是信号,信号就是一条消息,它通知进程发生了一个某种类型的事件,信号的实现稍微复杂一点,大概就是通过一个pcb中的pending信号集合,就是未决信号集合,本质上是一个位图,用来标记信号信息,信号产生之后会在位图中经历注册、注销、捕获处理的过程最终调用相应的信号处理函数来处理信号

第六种是SOCKET套接字,这种进程间通信机制可以用于不同主机之间的进程通信

20
Q

线程间通信

A

线程间通信一般说的是同进程之间的线程间的通信,因为不同进程之间的线程通信就可以归为进程通信。

首先就是在不同的线程之间可以建立一份公共资源,因为同属于一个进程,所以全局变量可以被访问到,但是要注意对临界资源访问的同步与互斥

第二种是互斥锁,可以保证公共资源不会被多个线程同时访问

第三种是信号量, 它可以用来实现同步,也就是通过对临界资源进行一个计数从而实现多个线程同一时刻访问同一个资源

第四种是信号,通过通知事件的方式来控制线程