Socket编程(三)——多进程
考完上学期的期末考试了,这学期课还特别少,最近集中精力学完这本书,开始泄洪式更新。
进程和僵尸进程
温故而知新:进程的概念
一个简单且易于理解的定义:“进程是占用内存空间的正在运行的程序。“进程是操作系统进行资源调度的基本单位,资源包括时间、内存和外设等。在网络编程中,在服务端实现多进程可以降低服务的平均时延,提高客户端的访问体验。在Linux操作系统下,可以通过下面这个函数来复制一个进程副本。
1 |
|
复制后的进程与父进程使用同一个内存空间,但是父进程中该函数的返回值是子进程的ID,子进程中该函数的返回值为0,可以利用这个特点来区分父子进程。
僵尸进程
当父进程创建一个子进程,并且子进程运行结束,产生返回值时,子进程会把这个返回值传递给操作系统。操作系统会试图将这个返回值传递给父进程,在此之前,操作系统都不会杀死子进程。处在这种运行完毕但没有被杀死的进程就是僵尸进程。很显然,僵尸进程会像其他进程一样占用资源,且不做任何事,所以应该避免僵尸进程的出现。
知道僵尸进程产生的原因后,就知道如何防止其产生。只需要在父进程中主动要求获得子进程的返回值即可。
方式一:wait函数
1 |
|
wait函数会让父进程进入阻塞状态,直到有子进程终止,此函数还要用两个宏函数配合使用:
1 |
|
方式二:waitpid函数
1 |
|
信号量
上面提到的两个方法都是父进程在主动等待子进程,更精明的方法是求助操作系统,向操作系统“注册”一个函数,用于收到信号量时执行。
signal
1 |
|
声明过于复杂,但是不难理解:
- 函数名:signal
- 参数:int signo, void(*func)(int)
- 返回值:参数为int,返回void型的函数指针
第一个参数用于传递常量,代表不同的事件,第二个参数代表事件发生时应该调用的函数。常量有下:
- SIGALRM:已到通过alarm函数注册的时间(定时器)
- SIGINT:输入CTRL+C
- SIGCHLD:子进程终止
alarm函数声明如下:
1 |
|
返回值似乎很令人迷惑,实际上如果给这个函数传递的参数为0,就会取消对SIGALRM信号的预约,此时可以通过读取返回值来确定剩余的或者已经过去的时间。
sigaction
实际上现在很少使用signal函数,因为sigaction在不同版本的Unix操作系统中完全相同。
1 |
|
sigaction函数操作较为复杂,但无论是可移植性还是灵活性都比signal函数强一些。
多进程服务端
多进程服务端的框架书上介绍得很详细,这里只记录一下其框架:
1 |
|
该段伪代码使用fork出的子进程来负责与客户端进行通信,服务端只负责接受客户端的连接请求。需要注意的是在子进程中需要事先关闭不再使用的文件描述符(实际上就是套接字),因为子进程中会复制父进程的文件描述符,而套接字只有当其所有文件描述符都关闭时才会被关闭。所以,如果子进程不事先关闭服务端套接字,父进程就有可能无法关闭套接字。
对于父进程中关闭客户端文件描述符的操作,也是因为此。
分割客户端的IO进程
对于客户端,使用多进程也可以提高程序执行的效率。书中例子将客户端的发送数据部分放在父进程中,读取回声放在子进程中,这样可以使得客户端的输入和输出在时间上无关,即下一轮的输出无需等待上一轮的输入到达。
进程间通信
以上提到的例子中其实已经设计到一部分的进程通信,但通信的方法是信号量,只能起到相互通知的作用。如果希望在进程之间读取较大规模的数据,需要用到一些其他技巧,例如管道。其实这个部分应该属于操作系统范畴,但是鉴于我寒假读了《操作系统导论》这本书却没有写读书笔记,这里再啰嗦一下罢。
1 |
|
filedes
这个数组我们可以像文件描述符一样使用,调用像write
、read
这样的IO函数来进行数据传输。根据pipe函数的描述我们可以知道,一个管道只能实现子进程到父进程的单向通信。
如果使用一个管道实现双向通信,可能会引发一些问题,因为“向管道传递数据时,先读的进程会把数据取走”,规划父子进程间数据的读取数据这件事十分繁琐。
总结
多进程的服务端和客户端可以在一定程度上优化程序的执行,但也会带来一些问题,例如占用内存较大等等。所以在实现大型服务端时,通常不会采用这种方式,本部分的学习可以权当是对操作系统知识点的一些回顾。