进程进阶

文件锁

进程锁

记录一个PID文件,避免两个进程同时运行的文件锁。

进程锁的作用之一就是可以协调进程的运行,例如crontab使用进程锁解决冲突提到,使用crontab限定每一分钟执行一个任务,但这个进程运行时间可能超过一分钟,如果不用进程锁解决冲突的话两个进程一起执行就会有问题。后面提到的项目实例Run也有类似的问题,通过进程锁可以解决进程间同步的问题。

使用PID文件锁还有一个好处,方便进程向自己发停止或者重启信号。Nginx编译时可指定参数–pid-path=/var/run/nginx.pid,进程起来后就会把当前的PID写入这个文件,当然如果这个文件已经存在了,也就是前一个进程还没有退出,那么Nginx就不会重新启动。

使用进程锁

进程锁在特定场景是非常适用的,而操作系统默认不会为每个程序创建进程锁,那我们该如何使用呢?

其实要实现一个进程锁很简单,通过文件就可以实现了。例如程序开始运行时去检查一个PID文件,如果文件存在就直接退出,如果文件不存在就创建一个,并把当前进程的PID写入文件中。这样我们很容易可以实现读锁,但是所有流程都需要自己控制。

当然根据DRY(Don’t Repeat Yourself)原则,Linux已经为我们提供了flock接口。

使用Flock

Flock提供的是advisory lock,也就是建议性的锁,其他进程实际上也可以读写这个锁文件。Linux上可以直接使用flock命令,使用C可以调用原生的flock接口.

1
2
3
4
5
6
7
8
9
10
11
12
$fp = fopen("/tmp/lock.txt", "r+");

if (flock($fp, LOCK_EX)) { // 进行排它型锁定
ftruncate($fp, 0); // truncate file
fwrite($fp, "Write something here\n");
fflush($fp); // flush output before releasing the lock
flock($fp, LOCK_UN); // 释放锁定
} else {
echo "Couldn't get the lock!";
}

fclose($fp);

孤儿进程

孤儿进程概念

根据维基百科的解释,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。

孤儿进程与僵尸进程是完全不同的。孤儿进程借用了现实中孤儿的概念,也就是父进程不在了,子进程还在运行,这时我们就把子进程的PPID设为1。前面讲PID提到,操作系统会创建进程号为1的init进程,它没有父进程也不会退出,可以收养系统的孤儿进程。

作用:

在现实中用户可能刻意使进程成为孤儿进程,这样就可以让它与父进程会话脱钩,成为守护进程。

僵尸进程

当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

守护(Daemon)进程

我们可以认为守护进程就是后台服务进程,因为它会有一个很长的生命周期提供服务,关闭终端不会影响服务,也就是说可以忽略某些信号。

*进程间通信

管道(Pipe)

管道是进程间通信最简单的方式,任何进程的标准输出都可以作为其他进程的输入。

信号(Signal)

信号是进程间通信的其中一种方法,当然也可以是内核给进程发送的消息,注意信息只是告诉进程发生了什么事件,而不会传递任何数据。

这是进程这个概念设计时就考虑到的了,因为我们希望控制进程,就像一个小孩我们想他按我们的想法做,前提就是他能够接受信号并且理解信号的含义。

信号种类
我们可以通过下面的命令来查当前系统支持的种类。

1
kill -l

其中1至31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),32到63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

在命令行中止一个程序我们一般摁Ctrl+c,这就是发送SIGINT信号,而使用kill命令呢?默认是SIGTERM,加上-9参数才是SIGKILL。

消息队列(Message)

和传统消息队列类似,但是在内核实现的。

共享内存(Shared Memory)

后面也会有更详细的介绍。

信号量(Semaphore)

信号量本质上是一个整型计数器,调用wait时计数减一,减到零开始阻塞进程,从而达到进程、线程间协作的作用。

套接口(Socket)

也就是通过网络来通信,这也是最通用的IPC,不要求进程在同一台服务器上。

系统调用

我们要想启动一个进程,需要操作系统的调用(system call)。实际上操作系统和普通进程是运行在不同空间上的,操作系统进程运行在内核态(todo: kernel space),开发者运行得进程运行在用户态(todo: user space),这样有效规避了用户程序破坏系统的可能。

如果用户态进程想执行内核态的操作,只能通过系统调用了。Linux提供了超多系统调用函数.

文件描述符

Linux很重要的设计思想就是一切皆文件,网络是文件,键盘等外设也是文件,很神奇吧?于是所有资源都有了统一的接口,开发者可以像写文件那样通过网络传输数据,我们也可以通过/proc/的文件看到进程的资源使用情况。

内核给每个访问的文件分配了文件描述符(File Descriptor),它本质是一个非负整数,在打开或新建文件时返回,以后读写文件都要通过这个文件描述符了。

应用

我们想想操作系统打开的文件这么多,不可能他们共用一套文件描述符整数吧?这样想就对了,Linux实现时这个fd其实是一个索引值,指向每个进程打开文件的记录表。

POSIX已经定义了STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO三个常量,也就是0、1、2。这三个文件描述符是每个进程都有的,这也解释了为什么每个进程都有编号为0、1、2的文件而不会与其他进程冲突。

文件描述符帮助应用找到这个文件,而文件的打开模式等上下文信息存储在文件对象中,这个对象直接与文件描述符关联。

Epoll

Epoll是poll的改进版,更加高效,能同时处理大量文件描述符,跟高并发有关,Nginx就是充分利用了epoll的特性。

Poll

Poll本质上是Linux系统调用,其接口为int poll(struct pollfd *fds,nfds_t nfds, int timeout),作用是监控资源是否可用。

举个例子,一个Web服务器建了多个socket连接,它需要知道里面哪些连接传输发了请求需要处理,功能与select系统调用类似,不过poll不会清空文件描述符集合,因此检测大量socket时更加高效。

Epoll

大幅提升了高并发服务器的资源使用率。前面提到poll会轮询整个文件描述符集合,而epoll可以做到只查询被内核IO事件唤醒的集合.

C10K问题,指的是服务器如何支持同时一万个连接的问题。如果是一万个连接就有至少一万个文件描述符,poll的效率也随文件描述符的更加而下降,epoll不存在这个问题是因为它仅关注活跃的socket。

实现

这是怎么做到的呢?简单来说epoll是基于文件描述符的callback函数来实现的,只有发生IO时间的socket会调用callback函数,然后加入epoll的Ready队列。更多实现细节可以参考Linux源码

Mmap

无论是select、poll还是epoll,他们都要把文件描述符的消息送到用户空间,这就存在内核空间和用户空间的内存拷贝。其中epoll使用mmap来共享内存,提高效率。

Mmap不是进程的概念,这里提一下是因为epoll使用了它,这是一种共享内存的方.

共享内存

不同进程之间内存空间是独立的,也就是说进程不能访问也不会干扰其他进程的内存。如果两个进程希望通过共享内存的方式通信呢?可以通过mmap()系统调用实现。

写时复制(Copy On Write)

一般我们运行程序都是Fork一个进程后马上执行Exec加载程序,而Fork的是否实际上用的是父进程的堆栈空间,Linux通过Copy On Write技术极大地减少了Fork的开销。

Copy On Write的含义是只有真正写的时候才把数据写到子进程的数据,Fork时只会把页表复制到子进程,这样父子进程都指向同一个物理内存页,只有再写子进程的时候才会把内存页的内容重新复制一份。

纵有疾风起,人生不言弃!