操作系统(2)

23、操作系统经典问题之读者-写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

typedef int semaphore;  
semaphore count_mutex = 1;  
semaphore data_mutex = 1;  
int count = 0;  
void reader() {  
    while(TRUE) {  
        down(&count_mutex);  
        count++;  
        if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问  
        up(&count_mutex);  
        read();  
        down(&count_mutex);  
        count--;  
        if(count == 0) up(&data_mutex);//最后一个读者要对数据进行解锁,防止写进程无法访问  
        up(&count_mutex);  
    }  
}  
void writer() {  
    while(TRUE) {  
        down(&data_mutex);  
        write();  
        up(&data_mutex);  
    }  
}

24、介绍一下几种典型的锁?

读写锁

多个读者可以同时进行读

写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

[**24.1、你知道哪几种线程锁(POSIX)?
互斥锁(mutex)

互斥锁属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程A和B,它们分别运行在core 0和core 1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞,此时会通过上下文切换将线程A置于等待队列中,此时core 0就可以运行其他的任务(如线程C)。

条件变量(cond)

自旋锁(spin)

自旋锁属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,如果自旋锁已经被线程B所持有,那么线程A就会一直在core 0上进行忙等待并不停的进行锁请求,检查该自旋锁是否已经被线程B释放,直到得到这个锁为止。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。

虽然它的效率比互斥锁高,但是它也有些不足之处:

自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低。

在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。

自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

25、逻辑地址VS物理地址

Eg:编译时只需确定变量x存放的相对地址是100 ( 也就是说相对于进程在内存中的起始地址而言的地址)。CPU想要找到x在内存中的实际存放位置,只需要用进程的起始地址+100即可。 相对地址又称逻辑地址,绝对地址又称物理地址。

26、怎么回收线程?有哪几种方法?

等待线程结束:int pthread_join(pthread_t tid, void** retval);

主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。

tid:创建线程时通过指针得到tid值。

retval:指向返回值的指针。

结束线程:pthread_exit(void *retval);

子线程执行,用来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。

retval:同上。

分离线程:int pthread_detach(pthread_t tid);

主线程、子线程均可调用。主线程中pthread_detach(tid),子线程中pthread_detach(pthread_self()),调用后和主线程分离,子线程结束时自己立即回收资源。

tid:同上。

27、内存的覆盖是什么?有什么特点?

由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成为一个固定区和若干个覆盖区。将经常活跃的部分放在固定区,其余部分按照调用关系分段,首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统将其调入覆盖区,替换覆盖区中原有的段。

覆盖技术的特点:是打破了必须将一个进程的全部信息装入内存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行,再而,大家要注意到,内存中能够更新的地方只有覆盖区的段,不在覆盖区的段会常驻内存。

28、内存交换是什么?有什么特点?

交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)

换入:把准备好竞争CPU运行的程序从辅存移到内存。 换出:把处于等待状态(或CPU调度原则下被剥夺运行权力)的程序从内存移到辅存,把内存空间腾出来。

29、什么时候会进行内存的交换?

内存交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出。

30、终端退出,终端运行的进程会怎样

终端在退出时会发送SIGHUP给对应的bash进程,bash进程收到这个信号后首先将它发给session下面的进程,如果程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出

31、如何让进程后台运行

(1)命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了

(2)ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程

(3)nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断(SIGHUP)信号

(4)运行指令前面 + setsid,使其父进程编程init进程,不受HUP信号的影响

(5)将 命令+ &放在()括号中,也可以是进程不受HUP信号的影响

32、什么是快表,你知道多少关于快表的知识?

快表,又称联想寄存器(TLB) ,是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。与此对应,内存中的页表常称为慢表。

33、地址变换中,有快表和没快表,有什么区别?

地址变换过程访问一个逻辑地址的访存次数
基本地址变换机构①算页号、页内偏移量 ②检查页号合法性 ③查页表,找到页面存放的内存块号 ④根据内存块号与页内偏移量得到物理地址 ⑤访问目标内存单元两次访存
具有快表的地址变换机构①算页号、页内偏移量 ②检查页号合法性 ③查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤;若未命中则进行④ ④查页表,找到页面存放的内存块号,并且将页表项复制到快表中 ⑤根据内存块号与页内偏移量得到物理地址 ⑥访问目标内存单元快表命中,只需一次访存 快表未命中,需要两次访存

35、 守护进程、僵尸进程和孤儿进程

守护进程

指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等

创建守护进程要点:

(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。

(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。

(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。

(4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。

(5)将当前目录更改为根目录。

(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。

(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。

孤儿进程

如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程

如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。

设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。

36、如何避免僵尸进程?

通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。

如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。

通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

37、局部性原理你知道吗?主要有哪两大局部性原理?各自是什么?

主要分为时间局部性和空间局部性

时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环) 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)

38、父进程、子进程、进程组、作业和会话

父进程

已创建一个或多个子进程的进程

子进程

由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

子进程从父进程继承的有:1.进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))2.环境(environment)3.堆栈4.内存5.进程组号

独有:1.进程号;2.不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到);3.资源使用(resource utilizations)设定为0

进程组

进程组就是多个进程的集合,其中肯定有一个组长,其进程PID等于进程组的PGID。只要在某个进程组中一个进程存在,该进程组就存在,这与其组长进程是否终止无关。

作业

shell分前后台来控制的不是进程而是作业(job)或者进程组(Process Group)。

一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,shell可以运行一个前台作业和任意多个后台作业,这称为作业控制

为什么只能运行一个前台作业? 答:当我们在前台新起了一个作业,shell就被提到了后台,因此shell就没有办法再继续接受我们的指令并且解析运行了。 但是如果前台进程退出了,shell就会有被提到前台来,就可以继续接受我们的命令并且解析运行。

作业与进程组的区别:如果作业中的某个进程有创建了子进程,则该子进程是不属于该作业的。 一旦作业运行结束,shell就把自己提到前台(子进程还存在,但是子进程不属于作业),如果原来的前台进程还存在(这个子进程还没有终止),他将自动变为后台进程组

会话

会话(Session)是一个或多个进程组的集合。一个会话可以有一个控制终端。在xshell或者WinSCP中打开一个窗口就是新建一个会话。

39、进程终止的几种方式

1、main函数的自然返回,return

2、调用exit函数,属于c的函数库

3、调用_exit函数,属于系统调用

4、调用abort函数,异常程序终止,同时发送SIGABRT信号给调用进程。

5、接受能导致进程终止的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)

exit和_exit的区别

40、Linux中异常和中断的区别

中断

大家都知道,当我们在敲击键盘的同时就会产生中断,当硬盘读写完数据之后也会产生中断,所以,我们需要知道,中断是由硬件设备产生的,而它们从物理上说就是电信号,之后,它们通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这定义在内核中),最后,由CPU发送给内核,有内核处理中断。下面这张图显示了中断处理的流程:

异常

我们在学习《计算机组成原理》的时候会知道两个概念,CPU处理程序的时候一旦程序不在内存中,会产生缺页异常;当运行除法程序时,当除数为0时,又会产生除0异常。所以,大家也需要记住的是,异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常,下面这张图显示了异常处理的流程:

相同点

最后都是由CPU发送给内核,由内核去处理

处理程序的流程设计上是相似的

不同点

产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的

内核需要根据是异常还是中断调用不同的处理程序

中断不是时钟同步的,这意味着中断可能随时到来;异常由于是CPU产生的,所以它是时钟同步的

当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中

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

通过这张图你可以看到,用户空间内存,从低到高分别是 7 种不同的内存段:

程序文件段,包括二进制可执行代码;

已初始化数据段,包括静态常量;

未初始化数据段,包括未初始化的静态变量;

堆段,包括动态分配的内存,从低地址开始向上增长;

文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关)

栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;

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

1、栈区(stack)— 地址向下增长,由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的队列,先进后出。

2、堆区(heap)— 地址向上增长,一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放

4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放

5、程序代码区(text)—存放函数体的二进制代码。

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

Linux环境下有操作系统决定,一般是8KB,8192kbytes,通过ulimit命令查看以及修改

Windows环境下由编译器决定,VC++6.0一般是1M

Linux

linux下非编译器决定栈大小,而是由操作系统环境决定,默认是8192KB(8M);而在Windows平台下栈的大小是被记录在可执行文件中的(由编译器来设置),即:windows下可以由编译器决定栈大小,而在Linux下是由系统环境变量来控制栈的大小的。

在Linux下通过如下命令可查看和设置栈的大小:

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

Windows

下程序栈空间的大小,VC++ 6.0 默认的栈空间是1M。

VC6.0中修改堆栈大小的方法:

选择 "Project->Setting"

选择 "Link"

选择 "Category"中的 "Output"

在 "Stack allocations"中的"Reserve:"中输栈的大小

44、程序从堆中动态分配内存时,虚拟内存上怎么操作的

页表:是一个存放在物理内存中的数据结构,它记录了虚拟页与物理页的映射关系

在进行动态内存分配时,例如malloc()函数或者其他高级语言中的new关键字,操作系统会在硬盘中创建或申请一段虚拟内存空间,并更新到页表(分配一个页表条目(PTE),使该PTE指向硬盘上这个新创建的虚拟页),通过PTE建立虚拟页和物理页的映射关系。

45、常见的几种磁盘调度算法

读写一个磁盘块的时间的影响因素有:

旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)

寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)

实际的数据传输时间

其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。

1. 先来先服务

按照磁盘请求的顺序进行调度。

优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。

2. 最短寻道时间优先

优先调度与当前磁头所在磁道距离最近的磁道。

虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。

3. 电梯扫描算法

电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。

电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。

因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。

46、交换空间与虚拟内存的关系

交换空间 Linux 中的交换空间(Swap space)在物理内存(RAM)被充满时被使用。如果系统需要更多的内存资源,而物理内存已经充满,内存中不活跃的页就会被移到交换空间去。虽然交换空间可以为带有少量内存的机器提供帮助,但是这种方法不应该被当做是对内存的取代。交换空间位于硬盘驱动器上,它比进入物理内存要慢。 交换空间可以是一个专用的交换分区(推荐的方法),交换文件,或两者的组合。 交换空间的总大小应该相当于你的计算机内存的两倍和 32 MB这两个值中较大的一个,但是它不能超过 2048MB(2 GB)。 虚拟内存 虚拟内存是文件数据交叉链接的活动文件。是WINDOWS目录下的一个"WIN386.SWP"文件,这个文件会不断地扩大和自动缩小。 就速度方面而言,CPU的L1和L2缓存速度最快,内存次之,硬盘再次之。但是虚拟内存使用的是硬盘的空间,为什么我们要使用速度最慢的硬盘来做 为虚拟内存呢?因为电脑中所有运行的程序都需要经过内存来执行,如果执行的程序很大或很多,就会导致我们只有可怜的256M/512M内存消耗殆尽。而硬盘空间动辄几十G上百G,为了解决这个问题,Windows中运用了虚拟内存技术,即拿出一部分硬盘空间来充当内存使用。

47、抖动你知道是什么吗?它也叫颠簸现象

刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)

为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率 为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程工作集” 的概念

评论区 0