操作系统
什么是守护进程、以及如何创建守护进程
守护进程(daemon进程)是一个长时间运行并且没有控制终端的进程(即它运行在后台)。守护进程与后台进程还是有区别的:守护进程完全脱离终端,而后台程序未完全脱离终端。守护进程在关闭终端时不受影响,而后台程序一般随用户退出而停止(以
nohup command &
格式运行可以避免)。守护进程的会话组、当前目录、文件描述符都是独立的,后台程序知识终端进行了一次fork
,让程序在后台运行,这些都没改变。一般的网络服务都是以守护进程的方式运行。守护进程的名字通常以
d
结尾(不是标准)。创建一个守护进程的过程:
1)执行一个
fork
,之后父进程退出,子进程在后台继续执行(结果是daemon
成了init进程的子进程)。2)子进程调用
setsid()
开启一个新的会话,并释放它与控制终端之间的所有关联。(调用setsid
会使子进程成为新的会话组首领和新的进程组首领,并失去控制终端)3)避免
daemon
重新打开一个终端设备:在所有可能应用到一个终端设备上的open()
调用中指定O_NOCTTY
标记。或者简单说,在setsid()
调用后执行第二个fork()
,让子进程退出,让孙子进程继续执行。这样就确保孙子进程不会成为会话组领导,根据System V中获取终端的规则(Linux也遵循),进程将永远不会重新请求一个控制终端。4)清除进程
umask
,以确保daemon创建文件和目录时拥有所需的权限。5)更改当前工作目录为根目录(
/
)。因为daemon通常运行到系统关闭,如果其当前目录不包含/
的文件系统,那么就无法卸载该文件系统。6)关闭从父进程继承来的所有打开着的文件描述符。由于daemon失去了控制终端并且在后台运行,因此让daemon保持文件描述符0、1、2的打开状态没有意义,因为它们就是指向控制终端。可以重定向到
/dev/null
上。(/dev/null
是一个虚拟设备,它总会将写入的数据丢弃,当成无底洞即可)(避免调用的一些库函数会向标准输出上输出一些信息)(文件描述符是一种有限的资源)7)用
openlog
函数建立与syslogd
的连接。(daemon在后台运行,没法像普通程序将消息输出到关联的终端,所以可以将消息写入到一个特定于应用程序的日志文件中)daemon应该在合适的地方正确的处理
SIGTERM
和SIGHUP
信号,SIGTERM
信号的处理方式应该是按序关闭这个daemon,而SIGHUP
信号则提供了一种机制让daemon通过读取配置文件并重新打开所使用的日志文件来重新初始化自身。参考:《Linux/Unix系统编程手册》第37章
用户态、内核态
Linux操作系统的体系架构分为用户态和内核态(用户空间和内核),内核本质上是一种软件,控制着计算机硬件资源,并为上层应用程序提供运行环境。用户态即上层应用的活动空间,通过访问内核提供的接口:系统调用,来访问内核控制的资源。系统调用是操作系统的最小功能单元,linux提供了很多系统调用。而库函数则是对这些系统调用的封装,让程序员可以从复杂的细节中解脱。
用户态切换到内核态的三种情况:a. 系统调用。b. 异常事件(如缺页)。c. 外围设备中断。系统调用本质也是中断,相对于外围设备的硬中断,其称为软中断。三种切换方式相同,都是执行了一个中断响应过程。只有系统调用是主动请求切换,其余则是被动。
进程间通信
1)管道:管道的实质是一个内核缓冲区(调用 pipe 函数来开辟),管道的作用正如其名,需要通信的两个进程在管道的两端,进程利用管道传递信息。管道对于管道两端的进程而言,就是一个文件,但是这个文件比较特殊,它不属于文件系统并且只存在于内存中。一般指匿名管道,其有几个重要的限制:a. 管道是半双工的,数据只能在一个方向上流动。b. 管道只能用于父子进程或兄弟进程之间的通信,即具有亲缘关系的进程。
2)FIFO:FIFO是一个先进先出的队列,类似管道,只允许数据单向流动。每个FIFO都有一个名字,因此也被称为命名管道。命名管道允许没有亲缘关系的进程进行通信,命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,其是一个设备文件,这样一个进程即使与创建有名管道的进程不存在亲缘关系,只要可以访问该路径,就能通过有名管道互相通信。
3)信号量:一种用于提供不同进程之间或者一个给定的不同线程间同步手段的原语。信号量多用于进程间的同步与互斥,简单的说一下同步和互斥的意思:
同步:处理竞争就是同步,安排进程执行的先后顺序就是同步,每个进程都有一定的先后执行顺序。
互斥:互斥访问不可共享的临界资源,同时会引发两个新的控制问题(互斥可以说是特殊的同步)。
竞争:当并发进程竞争使用同一个资源的时候,我们就称为竞争进程。
共享资源通常分为两类:一类是互斥共享资源,即任一时刻只允许一个进程访问该资源;另一类是同步共享资源,即同一时刻允许多个进程访问该资源;信号量是解决互斥共享资源的同步问题而引入的机制。
当有进程要求使用共享资源时,需要执行以下操作:a.系统首先要检测该资源的信号量;b.若该资源的信号量值大于 0,则进程可以使用该资源,此时,进程将该资源的信号量值减1;c.若该资源的信号量值为 0,则进程进入休眠状态,直到信号量值大于 0 时进程被唤醒,访问该资源;
4)消息队列:是UNIX下不同进程之间可以实现共享资源的一种机制,允许不同进程将格式化的数据流以消息队列形式发送给任意进程;消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列 ID)来标识。其具有以下特点:
a.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
b.消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
c.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
5)共享内存:顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。
在 Linux 中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
6)套接字:套接字是更为基础的进程间通信机制,与其他方式不同的是,套接字可用于不同机器之间的进程间通信。
有两种类型的套接字:基于文件的和面向网络的。
- Unix 套接字是基于文件的,并且拥有一个家族名字:AF_UNIX,它代表地址家族 (address family):UNIX。
- 第二类型的套接字是基于网络的,它也有自己的家族名字:AF_INET,代表地址家族 (address family):INTERNET
不管采用哪种地址家族,都有两种不同的套接字连接:面向连接的和无连接的。
面向连接的套接字 (SOCK_STREAM):进行通信前必须建立一个连接,面向连接的通信提供序列化的、可靠地和不重复的数据交付,而没有记录边界。这意味着每条信息可以被拆分成多个片段,并且每个片段都能确保到达目的地,然后在目的地将信息拼接起来。实现这种连接类型的主要协议是传输控制协议 (TCP)。
无连接的套接字 (SOCK_DGRAM):在通信开始之前并不需要建立连接,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段。由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”实现这种连接类型的主要协议是用户数据报协议 (UDP)。
进程和线程
进程是具有一定功能的程序的一个运行实例,进程是系统进行资源调度和分配的一个独立单位。
线程是进程的实体,是CPU调度和分派的基本单位,它比进程更小的能独立运行的基本单位。
一个进程可以有多个线程,多个线程也可以并发执行。
区别:
拥有资源:进程是资源分配的基本单位,线程不拥有资源,线程可以访问隶属于进程的资源。
调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程的切换,从一个进程的线程切换到另一个进程中的线程,会引起进程切换。
系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程CPU环境的保存及CPU环境的设置,而线程切换只需保存和设置少量的寄存器的内容,开销很小。
通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。
线程同步
互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
信号量:它允许同一时刻多个线程访问统一资源,但是需要控制同一时刻访问资源的最大线程数量。
事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
什么是死锁,死锁产生的条件
有两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的所就是两个或多个进程无限期的阻塞、相互等待的一种状态。
死锁产生的是个条件:
互斥条件:一个资源一次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的环型等待资源关系
进程有哪几种状态
创建
就绪状态:进程已获得除处理器以外的所需资源,等待分配处理器资源
运行状态:占用处理器资源运行,处于此状态的进程数小于等于CPU数
阻塞状态:进程等待某种条件,在条件满足之前无法执行
终止
运形态->阻塞态:往往由于等待I/O等
阻塞态->就绪态:等待条件已满足,只需等待分配处理器
运行态->就绪态:外界原因导致退出运行状态,如时间片用完或更高级的进程抢占
就绪态->运行态:系统按照某种策略选中了就绪队列中的一个进程占用处理器
进程调度算法有哪些
先来先服务 非抢占式的调度算法,按照请求的顺序进行调度。 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。另外,对
I/O
密集型进程也不利,因为这种进程每次进程I/O
操作之后又得重新排队。短作业优先 非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 长作业可能会等待很长时间都得不到服务。因为一旦有短作业到来,就会优先服务短作业。
最短剩余时间优先 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达,其整个运行时间与当前进程的剩余时间比较。如果新的进程需要的时间更少,这挂起当前进程,运行新的进程。否则新的进程等待。
时间片轮转 将所有的就绪进程按
FCFS
原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送到就绪队列的末尾,同时继续把时间片分配给队首进程。 时间片轮转算法的效率和时间片的大小有很大的关系:1.因为进程切换开销大,时间片太小会导致进程切换太频繁,在进程切换上就花费了很多时间。2.如果时间片过长,那么实时性就不能得到保障。优先级调度 为每个进程分配一个优先级,按优先级进程调度。 为了防止低优先级的进程永远得不到调度,可以随着时间的推移增加等待进程的优先级。
动态创建子进程或子线程缺点(相比进程池线程池)
动态创建进程(线程)是比较耗费时间的,将导致较慢的客户响应
动态创建的子进程(子线程)通常只用来服务一个客户(除非特殊处理),这将导致系统上产生大量的细微进程(线程)。进程(线程)的切换将消耗大量的CPU时间
动态创建的子进程是当前进程的完整映像,当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统可用资源急剧下降,进而影响服务器性能
孤儿进程和僵尸进程
孤儿进程就是一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被
init
进程(进程ID为1)所收养,由init
完成对它们的状态收集工作。因为孤儿进程会被init
进程收养被处理后续,所以孤儿进程不会对系统造成影响。僵尸进程就是一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过
wait()
或waitpid()
获取子进程信息后才会释放。如果子进程退出,而父进程没有调用wait()
或waitpid()
,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程。僵尸进程通过ps
命令显示出来的状态为Z
。 系统所能使用的进程号时有限的,如果产生大量的僵尸进程,可能会因为没有可用的进程号而导致系统不能产生新的进程。如果要消灭系统中大量的僵尸进程,只需将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被init
进程收养,init
就会释放所有僵尸进程所占有的资源,从而结束僵尸进程。