socket网络编程实现并发服务器——多进程

前言:对之前多进程服务器编写的简单总结。

1.什么是进程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,信号 ,消息队列等。

2.创建进程

2.1 fork()系统调用

Linux下有两个基本的系统调用可以用于创建子进程:fork()和vfork()。英文fork是"分叉"的意思。可以这样理解:一个进程在运行中,如果调用了fork(),就产生了另一个进程,于是进程就”分叉”了。在我们编 程的过程中,一个函数调用只有一次返回(return),但由于fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回 是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()后,需要通 过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进 程在运行,而如果返回值<0的话,说明fork()系统调用出错。fork 函数调用失败的原因主要有两个:

  1. 系统中已经有太多的进 程;
  2. 该实际用户 ID 的进程总数超过了系统限制。

fork函数原型

#include <unistd.h>

pid_t fork(void);
 /*fork()系统调用会创建一个新的进程,这时它会有两次返回。
       一次返回是给父进程,其返回值是子进程的PID(Process ID),
       第二次返回是给子进程,其返回值为0。*/

2.2 父进程创建子进程过程

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

int g_var = 1;
char g_buf[] = "hello world\n";

int main (int argc, char **argv)
{
      int     var = 88; 11         pid_t   pid; 
      if( write(STDOUT_FILENO, g_buf, sizeof(g_buf)-1) < 0)
      {
            printf("Write string to stdout error: %s\n", strerror(errno));                      
            return -1; 
      }
      printf("创建 fork\n");
       
      if ((pid = fork()) < 0)
      {
            printf("fork() error: %s\n", strerror(errno));
            return -2;
      }
      else if ( 0 == pid)
      {
            printf("Child process PID[%d] running...\n", getpid());
            g_var++;
            var++;
      }
      else
      {
             
              printf("Parent process PID[%d] waiting...\n", getpid());
              sleep(1);
      }
      printf("PID=%ld, g_var=%d, var=%d\n", (long) getpid(), g_var, var);
      return 0;

在这里插入图片描述
在上面的编译运行过程我们可以看到,父进程在代码第21行创建了子进程后,系统会将父进程的文本段、数据段、堆栈都拷贝 一份给子进程,这样子进程也就继承了父进程数据段中的的全局变量g_var和局部变量var的值。

1,创建之后究竟是父进程还是子进程先运行?
因为并没有规定两者谁先运行,所以在代码的末端调用了sleep(1)的目的是希望让子进程 先运行。

2,为什么最后一行的printf被执行了两次?
这是因为fork()之后,子进程会复制父进程的代码段。而子进程在运行到var++后并没有调用return()或exit()函数让进程退出,所以程序会继续执行到倒数第二行调 用return 0退出子进程;同理父进程也是执行return 0 才让父进程退出,所以printf()分别被父子进程执行了两 次。

3,输入值的改变是否会影响父进程?
这个改变只影响子进程的空间的值,并不会影响父进程的内存空间,所 以子进程里g_var和var分别变成了2和101,而父进程的g_var和var都没改变;

4,简单的说系统调用fork()会创建一个新的子进程,这个子进程是父进程的一个副本。这也意味着,系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。这就好比你父母之前买了一套房子。等到你结婚了,又买了一套一模一样的房子给你,然后你对这套房子怎么装修都不会影响到你父母的房子。

5,从代码中我们不难看出,对于fork()函数, 在编程时,任何位置的exit()函数调用都会导致本进程(程序)退出,main()函数中的return()调用也会导致进程退出,而 其他任何函数中的return()都只是这个函数返回而不会导致进程退出。

2.3父进程与子进程

1.由子进程自父进程继承到:

进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))

环境(environment)变量

堆栈

内存

打开文件的描述符(注意对应的文件的位置由父子进程共享, 这会引起含糊情况)

执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描 述符设置,POSIX.1要求所有目录 流都必须在exec函数调用时关闭。更详细说明, 参见《APUE》 W. R. Stevens, 1993, 尤晋元等译(以下简称《高级编 程》), 3.13节和8.9节)

信号(signal)控制设定

nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级, 数值越小,优先级越高) 进程调度类别(scheduler class) (译者注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级, 根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优 先执行)

进程组号

对话期ID(Session ID) (译者注:译文取自《高级编程》,指:进程所属的对话期 (session)ID, 一个对话期包括一个或多 个进程组, 更详细说明参见《APUE》 9.5节)

当前工作目录

根目录 (根目录不一定是“/”,它可由chroot函数改变) 文件方式创建屏蔽字(file mode creation mask (umask))

资源限制

控制终端

2子进程所独有:

进程号

不同的父进程号(译者注: 即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)

自己的文件描述符和目录流的拷贝(译者注: 目录流由opendir函数创建,因其为顺序读取,顾称“目录流”)

子进程不继承父进程的进程,正文(text), 数据和其它锁定内存(memory locks) (译者注:锁定内存指被锁定的虚拟内存 页,锁定后, 不允许内核将其在必要时换出(page out), 详细说明参见《The GNU C Library Reference Manual》 2.2 版, 1999, 3.4.2节)

在tms结构中的系统时间(译者注:tms结构可由times函数获得, 它保存四个数据用于记录进程使用中央处理器 (CPU: Central Processing Unit)的时间,包括:用户时间,系统时间, 用户各子进程合计时间,系统各子进程合计时间)

资源使用(resource utilizations)设定为0

阻塞信号集初始化为空集(译者注:原文此处不明确, 译文根据fork函数手册页稍做修改)

不继承由timer_create函数创建的计时器

不继承异步输入和输出 父进程设置的锁(因为如果是排他锁,被继承的话就矛盾了)

2.4vfork()系统调用

vfork()是另外一个可以用来创建进程的函数,他与fork()的用法相同,也用于创建一个新进程。 但vfork()并不将父进程的地址 空间完全复制到子进程中,因为子进程会立即调用exec或exit(),于是也就不会引用该地址空间了。不过子进程再调用exec()或 exit()之前,他将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响 了父进程空间的数据可能会导致父进程的执行异常。此外,vfork()会保证子进程先运行,在他调用了exec或exit()之后父进程才 可能被调度运行。如果子进程依赖于父进程的进一步动作,则会导致死锁。

vfork()的函数原型和 fork原型一样:

#include <unistd.h>
#include <sys/types.h>

pid_t vfork(void);

2.5wait()与waitpid()

当一个进程正常或异常退出时,内核就会向其父进程发送SIGCHLD信号。因为子进程退出是一个异步事件,所以这种信号也 是内核向父进程发送的一个异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即将被执行的函数,父进程可以 调用wait()或waitpid()可以用来查看子进程退出的状态。

函数原型:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

在子进程终止前,wait()使其调用者阻塞,而waitpid()有一选项可使调用者不用阻塞。 waitpid()并不等待在其调用的之后的第一个终止进程,他有若干个选项,可以控制他所等待的进程。 如果一个已经终止、但其父进程尚未对其调用wait()进行处理(获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵死进程(zombie)。如果子进程已经终止,并且是一个僵死进程,则wait()立即返回该子进程的状态。所以我们在多进程编程时,最好调用wait()或waitpid()来解决僵死进程的问题。

此外,如果父进程在子进程退出之前退出了,这时候子进程就变成了孤儿进程。当然每一个进程都应该有一个独一无二的父进程,init进程就是这样的一个“慈父”,Linux内核中所有的子进程在变成孤儿进程之后都会被init进程“领养”,这也意味着孤儿进程的父进程最终会变成init进程。

3. 代码示例

代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值