网络socket——多进程编程

一、概括

要怎么概括一个进程?
我们使用vim编辑生成的C文件叫做源码,源码给程序员来看的但机器不识别,这时我们需要使用编译器gcc编译生成CPU可识别的二进制可执行程序并保存在存储介质上,这时编译生成的可执行程序只能叫做程序而不能叫进程。而一旦我们通过命令(./a.out)开始运行时,那正在运行的这个程序及其占用的资源就叫做进程了。

二、多进程编程相关函数

fork()函数

 #include <unistd.h>
 pid_t fork(void);

fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。需要通过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进程在运行,而如果返回值<0的话,说明fork()系统调用出错。fork 函数调用失败的原因主要有两个一是系统中已经有太多的进程;二是该实际用户 ID 的进程总数超过了系统限制。
每个进程都可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID。一个进程可以创建多个子进程,但父进程并没有一个API函数可以获取其子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。
通过一个简单的例子来演示过程

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <string.h>
 4 #include <errno.h>
 5
 6 int main(int argc, char **argv)
 7 {
 8      pid_t pid;
 9
10      printf("Parent process PID[%d] start running...\n", getpid() );
11
12      pid = fork();
13      if(pid < 0)
14      {
15      printf("fork() create child process failure: %s\n", strerror(errno));
16      return -1;
17      }
18      else if( pid == 0 )//子进程运行
19      {
20      printf("Child process PID[%d] start running, my parent PID is [%d]\n", getpid(),getppid());
21      return 0;
22      }
23      else //父进程运行
24      {
25      printf("Parent process PID[%d] continue running, and child process PID is [%d]\n",getpid(), pid);
26      return 0;
27      }
28 }

编译并且运行在这里插入图片描述
fork()系统调用会创建一个新的子进程,这个子进程是父进程的一个副本。这也意味着,系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执
行要看系统的进程调度策略。如果需要确保让父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来自己实现。
那子进程继承父进程哪些东西?
以下是子进程从父进程继承到的:
进程的资格
环境(environment)变量
堆栈
内存
打开文件的描述符
执行时关闭(close-on-exec)
信号(signal)控制设定
nice值 (该值表示进程的优先级, 数值越小,优先级越高)
进程调度类别(scheduler class) (注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)
进程组号
对话期ID(Session ID)
当前工作目录
根目录 (根目录不一定是“/”,它可由chroot函数改变)
文件方式创建屏蔽字(file mode creation mask (umask))
资源限制

子进程独有的
不同的父进程号(注: 即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)
自己的文件描述符和目录流的拷贝
子进程不继承父进程的进程
资源使用
阻塞信号集初始化为空集

exec*()函数

我们创建了一个子进程都是让子进程继续执行父进程的文本段,但更多的情况下是让该进程去执行另外一个程序。这时我们会在fork()之后紧接着调用exec*()系列的函数来让子进程去执行另外一个程序。
原型为:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

l(list) :表示以列表的形式传递要执行程序的命令行参数;
v(vector):表示以数组的形式传递要执行程序的命令行参数;
e(environment):表示给该命令传递环境变量;

参数:
path:启动的程序名包括路径;
arg:表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束
返回值:成功返回0,失败返回-1

vfork()函数

#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
pid_t vfork(void);

在fork()之后常会紧跟着调用exec来执行另外一个程序,而exec会抛弃父进程的文本段、数据段和堆栈等并加载另外一个程序,使用了写时复制技术: 这些数据区域由父子进程共享,内核将他们的访问权限改成只读,如果父进程和子进程中的任何一个试图修改这些区域的时候,内核再为修改区域的那块内存制作一个副本。
在调用vfork()之前,子进程将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响了父进程空间的数据可能会导致父进程的执行异常。

wait()函数与waitpid()函数

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

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

三,多进程改写服务器程序

流程图如下:
在这里插入图片描述

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <getopt.h>
#define MSG_STR "Hello Beauty\n"
void print_usage(char *progname)
{
     printf("%s usage: \n", progname);
     printf("-p(--port): sepcify server listen port.\n");
     printf("-h(--Help): print this help information.\n");
     return ;
}
int main(int argc, char **argv)
{
     int sockfd = -1;
     int rv = -1;
     struct sockaddr_in servaddr;
     struct sockaddr_in cliaddr;
     socklen_t len;
     int port = 0;
     int clifd;
     int ch;
     int on = 1;
     pid_t pid;
     struct option opts[] = {
        {"port", required_argument, NULL, 'p'},
        {"help", no_argument, NULL, 'h'},
        {NULL, 0, NULL, 0}
     };
     while( (ch=getopt_long(argc, argv, "p:h", opts, NULL)) != -1 )
     {
         switch(ch)
         {
             case 'p':
             port=atoi(optarg);//将字符串转化为数字;
             break;
             case 'h':
             print_usage(argv[0]);
             return 0;
         }
     }
     if( !port )
     {
     print_usage(argv[0]);
     return 0;
     }
     sockfd=socket(AF_INET, SOCK_STREAM, 0);
     if(sockfd < 0)
     {
     printf("Create socket failure: %s\n", strerror(errno));
     return -1;
     }
     printf("Create socket[%d] successfully!\n", sockfd);
     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));//重用端口;
     memset(&servaddr, 0, sizeof(servaddr));
     servaddr.sin_family=AF_INET;
     servaddr.sin_port = htons(port);
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 //inet_aton("192.168.0.17", &servaddr.sin_addr);
     rv=bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
     if(rv < 0)
     {
     printf("Socket[%d] bind on port[%d] failure: %s\n", sockfd, port,         strerror(errno));
     return -2;
     }
     listen(sockfd, 13);
     printf("Start to listen on port [%d]\n", port);
     while(1)
     {
     printf("Start accept new client incoming...\n");
     clifd=accept(sockfd, (struct sockaddr *)&cliaddr, &len);
     if(clifd < 0)
     {
     printf("Accept new client failure: %s\n", strerror(errno));
     continue;
     }
     printf("Accept new client[%s:%d] successfully\n",
     inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
     pid = fork();
     if( pid < 0 )
     {
     printf("fork() create child process failure: %s\n",  strerror(errno));
     close(clifd);
     continue;
     }
     else if( pid > 0 )
     {
 
     close(clifd);//父进程要处理下一个连接的client,所以要断开;
     continue;
     }
     else if ( 0 == pid )
     {
     char buf[1024];
 
     close(sockfd);
 
     printf("Child process start to commuicate with socket client...\n");
     memset(buf, 0, sizeof(buf));
     rv=read(clifd, buf, sizeof(buf));
     if( rv < 0 )
     {
          printf("Read data from client sockfd[%d] failure: %s\n", clifd,
strerror(errno));
          close(clifd);
          exit(0);
     }
     else if( rv == 0 )
     {
          printf("Socket[%d] get disconnected\n", clifd);
          close(clifd);
          exit(0);
     }
     else if( rv > 0 )
     {
           printf("Read %d bytes data from Server: %s\n", rv, buf);
     }
     rv=write(clifd, MSG_STR, strlen(MSG_STR));
     if(rv < 0)
     {
           printf("Write to client by sockfd[%d] failure: %s\n", sockfd,
strerror(errno));
           close(clifd);
           exit(0);
     }
     sleep(1);
     printf("close client socket[%d] and child process exit\n", clifd);
     close(clifd);
     exit(0);
     }
   }
 close(sockfd);
 return 0;
}

程序分析

父进程accept()接收到新的连接后,就调用fork()系统调用来创建子进程来处理与客户端的通信。因为子进程会继承父进程处于listen状态的socket 文件描述符(sockfd),也会继承父进程accept()返回的客户端socket 文件描述符(clifd),但子进程只处理与客户端的通信,这时他会将父进程的listen的文件描述符sockfd关闭;同样父进程只处理监听的事件,所以会将clifd关闭。 此时父子进程同时运行完成不同的任务,子进程只负责跟已经建立的客户端通信,而父进程只用来监听到来的socket客户端连接。所以当有新的客户端到来时,父进程就有机会来处理新的客户连接请求了,同时每来一个客户端都会创建一个子进程为其服务。
可以使用TCP socket测试工具(TCP Test Tool)连接并测试服务器的执行情况。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值