多进程编程
一、服务器并发访问的问题
服务器按处理方式可以分为迭代服务器和并发服务器两类。平常用C语言编写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很低,通常这种服务器被称为迭代服务器。
但实际上,不可能让一个服务器长时间地为一个客户服务,而是需要其具有同时处理多个客户请求的能力,这种可以同时处理多个客户端请求的服务器被称为并发服务器,其效率很高却实现有些复杂。在实际应用中,并发服务器应用的最广泛。
linux下有3种实现并发服务器的方式:多进程并发服务器,多线程并发服务器,IO多路复用。先来看多进程并发服务器的实现
二、多进程相关简介
1、什么是进程?
很多人认为可执行的程序就是进程,其实这个说法并不到位!进程这个概念针对的是操作系统,而不是针对用户。进程在操作系统原理是这样描述的:正在运行的程序及其占用的资源(CPU、内存、系统资源等)叫做进程。
2、进程空间内存布局
在深入了解多进程编程之前,我们首先要了解Linux下进程在运行时的内存布局。Linux 进程内存管理的对象都是虚拟内存,每个进程会有 0-4G 的虚拟内存空间,0-3G 是用户空间,用来执行用户自己的代码, 而高 1GB 的空间则是内核空间执行 Linux 系统调用,这里存放在整个内核的代码和所有的内核模块。
Linux下一个进程在内存里有三部分的数据,即”代码段”、”堆栈段”和”数据段”。学过汇编语言的同学应该知道,CPU一般都有上述三种段寄存器,这三个部分数据构成了一个完整执行序列的必要部分。
① 代码段:存放程序代码的数据
② 堆栈段:存放子程序的返回地址、子程序的参数以及程序的局部变量和malloc()动态申请内存的地址
③ 数据段:存放程序的全局变量,静态变量及常量
下图是Linux下进程的内存布局:
Linux 内存管理的基本思想就是只有在真正访问一个地址的时候才建立这个地址的物理映射,Linux C/C++语言的分配方式共有3 种:
(1)从静态存储区域分配。就是数据段的内存分配,这段内存在程序编译阶段就已经分配好,在程序的整个运行期间都存在,例如全局变量、static 变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是系统栈中分配的内存容量有限,比如大额数组就会把栈空间撑爆导致段错误。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc()或 new 申请任意多少的内存,程序员自己负责在何时用free ()或 delete 释放内存。此 区域内存分配称之为动态内存分配。动态内存的生存期由我们决定,使用非常灵活,但问题也最多,比如指向某个内存块的指针取值发生了变化又没有其他指针指向这块内存,这块内存就无法访问,发生内存泄露。
3、系统调用fork()
Linux下有两个基本的系统调用可以用于创建子进程:fork()和vfork()。英文fork是"分叉"的意思。可以这样理解:一个进程在运行中,如果调用了fork(),就产生了另一个进程,于是进程就”分叉”了!
fork()的函数原型及返回值:
#include <unistd.h>
pid_t fork(void);
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in
the child. On failure, -1 is returned in the parent, no child process is created, and
errno is set appropriately.
/*fork()系统调用会创建一个新的进程,这时它会有两次返回。
一次返回是给父进程,其返回值是子进程的PID(Process ID),
第二次返回是给子进程,其返回值为0。*/
系统调用fork()会创建一个新的子进程,这个子进程是父进程的一个副本。这也意味着,系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。这就好比你父母之前买了一套房子。等到你结婚了,又买了一套一模一样的房子给你,然后你对这套房子怎么装修都不会影响到你父母的房子!
另外,每个子进程只能有一个父进程,并且每个进程都可以通过调用getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID,这样在fork()时返回0给子进程是可取的。一个进程可以创建多个子进程,但对于父进程而言,他并没有API函数用来获取其子进程的进程PID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。这也是系统调用fork()设计两次返回值的原因。
因此在调用fork()后,需要通过其返回值来判断当前的代码是父进程还是子进程在运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进程在运行,而如果返回值<0的话,说明fork()系统调用出错。出错原因大致有以下两点:
① 系统中已经有太多的进程
② 该实际用户 ID 的进程总数超过了系统限制
关于进程的退出,我们知道在main()函数里使用return,就会调用exit()函数,从而导致进程退出。对于其他函数,在其任何位置调用exit()也会导致进程退出。因此,倘若子进程使用了return,那么子进程就会退出;同理,父进程使用了return,也会退出。这里我们需要注意的是:在编程时,在程序的任何位置调用exit()函数都会导致本进程退出;在main()函数中使用return,会导致进程退出;但在其他函数中使用return都只是令这个函数返回,而不会导致进程退出。
下面是一个简单的程序例子来描述父进程创建子进程的过程
示例代码如下:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
int g_var = 6;
char g_buf[] = "A string write to stdout.\n";
int main (int argc, char **argv)
{
int var = 88;
pid_t pid;
if (write(STDOUT_FILENO, g_buf, sizeof(g_buf)-1) < 0)
{
printf(