精华内容
下载资源
问答
  • 多进程并发服务器的工作机制
    千次阅读
    2022-03-23 15:57:48

    Experiment 0x2:多进程并发服务器TCP编程

    0x0 说明

    实验二:多进程并发服务器TCP编程

    记录实验课代码

    代码环境:

    win10

    VS2019 远程连接 ubuntu20

    进行linux编程

    0x1 要求

    要求:实现一个基于TCP协议的多进程并发通信服务器与客户端,要求完成以下功能

    1、基于多进程并发服务器编程模板,设计一个基于TCP协议的网络通信程序

    2、实现并发服务器的功能,子进程完成客户端和服务器之间的网络通信

    • 多进程编程
      • fork函数的使用;
      • wait,waitpid函数的使用;
    • 基于多进程实现的网络通信程序,含服务器和客户端:
      • 由主进程循环接收客户的连接请求,并显示客户的IP地址和端口号;
      • 由子进程独立负责不同客户端之间的数据通信与信息回显。

    0x2 实现

    实现一个基于TCP协议的多进程并发通信服务器与客户端,

    [外链图片转存失败,源站可能有防盗img](https://img-blog!链机制,建议将来csdnimcn/接上传在这里插入图片描述

    0x3 源码

    1- TCP服务端源码

    #include<stdio.h>
    #include<string.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    
    #define PORT 2345
    #define BACKLOG 5
    #define MAXDATASIZE 1000
    
    void process_client(int connfd, sockaddr_in client);
    
    int main()
    {
        int listenfd;
        sockaddr_in server;
        
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1) {
            perror("Creating socket failed.\n");
            _exit(1);
        }
        int opt = SO_REUSEADDR;
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        bzero(&server, sizeof(sockaddr_in));
        server.sin_family = AF_INET;
        server.sin_port = htons(PORT);
        server.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int len;
        len = sizeof(sockaddr_in);
        int flag = bind(listenfd, (sockaddr*)&server, len);
        if (flag == -1) {
            perror("bind() error!\n");
            _exit(1);
        }
    
        flag = listen(listenfd, BACKLOG);
        if (flag == -1) {
            perror("listen() error!\n");
            _exit(1);
        }
    
        len = sizeof(sockaddr_in);
        pid_t pid;
        while (1) {
            sockaddr_in client;
            int connfd = accept(listenfd, (sockaddr*)&client,(socklen_t*)&len);
            if (connfd == -1) {
                perror("accept() error!\n");
                _exit(1);
            }
            pid = fork();
            if (pid > 0) {//parent process
                close(connfd);
                continue;
            }
            else if (pid == 0) {//child process
                close(listenfd);
                process_client(connfd, client);
                _exit(0);
            }
            else {
                perror("fork() error!\n");
                _exit(1);
            }
    
        }
        close(listenfd);    
    
        return 0;
    }
    
    void process_client(int connfd, sockaddr_in client) {
    
        int num;
        char recvbuf[MAXDATASIZE] = { 0 };
        char sendbuf[MAXDATASIZE] = { 0 };
        char client_name[MAXDATASIZE];
        printf("You got a connection from IP:%s . port:%d\n", inet_ntoa(client.sin_addr) ,ntohs(client.sin_port));
    
        num = recv(connfd, client_name, MAXDATASIZE,0);
        if (num == 0) {
            close(connfd);
            printf("Client disconnected.\n");
            return;
        }
        client_name[num - 1] = '\0';
        printf("Client's name is %s.\n", client_name);
        while (num = recv(connfd, recvbuf, MAXDATASIZE,0) , num ) {
            recvbuf[num] = '\0';
            printf("Received client(%s) message: %s\n", client_name, recvbuf);
            /*
            int i = 0;
            for (i = 0; i < num; i++) {
                if ((recvbuf[i] >= 'a' && recvbuf[i] <= 'z') || (recvbuf[i] >= 'A' && recvbuf[i] <= 'Z')) {
                    recvbuf[i] += 3;
                    if ((recvbuf[i] > 'z' && recvbuf[i] <= 'z' + 3) || (recvbuf[i] > 'Z' && recvbuf[i] <= 'Z' + 3)) {
                        recvbuf[i] = recvbuf[i] - 26;
                    }
                }
                sendbuf[i] = recvbuf[i];
            }
            sendbuf[i] = '\0';
            */
            memcpy(sendbuf, recvbuf, strlen(recvbuf));
            send(connfd, sendbuf, strlen(recvbuf), 0);
    
            memset(sendbuf, 0, MAXDATASIZE);
            memset(recvbuf, 0, MAXDATASIZE);
            num = 0;
        }
        close(connfd);
    }
    
    

    2- TCP客户端源码

    #include<stdio.h>
    #include<unistd.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<netdb.h>
    
    #define PORT 2345
    #define MAXDATASIZE 100
    
    void process(FILE* fp, int sockfd);
    char* getMessage(char* sendline, int len, FILE* fp);
    int main(int argc, char* argv[]) {
    	int sockfd;
    	hostent* he;
    	sockaddr_in server;
    	
    	if (argc != 2) {
    		printf("Usage:%s<IP Address>\n", argv[0]);
    		_exit(1);
    	}
    	he = gethostbyname(argv[1]);
    	if (he == NULL) {
    		perror("gethostbyname() error\n");
    		_exit(1);
    	}
    	sockfd = socket(AF_INET, SOCK_STREAM, 0);
    	if (sockfd == -1) {
    		perror("socket() error!\n");
    		_exit(1);
    	}
    
    	bzero(&server, sizeof(sockaddr_in));
    	server.sin_family = AF_INET;
    	server.sin_port = htons(PORT);
    	server.sin_addr = *(in_addr*)he->h_addr_list[0];
    
    	int flag = connect(sockfd, (sockaddr*)&server, sizeof(sockaddr_in));
    	if (flag == -1) {
    		perror("connect() error!\n");
    		_exit(1);
    	}
    	process(stdin, sockfd);
    	close(sockfd);
    	
    }
    
    void process(FILE* fp, int sockfd) {
    	char sendline[MAXDATASIZE] = { 0 };
    	char recvline[MAXDATASIZE] = { 0 };
    
    	printf("Connected to server.\n");
    	printf("Input client's name: ");
    
    	char* flag = fgets(sendline, MAXDATASIZE, fp);
    	if (flag == NULL) {
    		perror("\nfgets() error! Exit()\n");
    		_exit(1);
    	}
    
    	send(sockfd, sendline, strlen(sendline), 0);
    	while (flag = getMessage(sendline, MAXDATASIZE, fp), flag) {
    		send(sockfd, sendline, strlen(sendline), 0);
    		int num;
    		num = recv(sockfd, recvline, MAXDATASIZE, 0);
    		if (num == 0) {
    			printf("Server terminated.\n");
    			return;
    		}
    		recvline[num] = '\0';
    		printf("Server Message: %s\n", recvline);
    
    		memset(sendline, 0, MAXDATASIZE);
    		memset(recvline, 0, MAXDATASIZE);
    	}
    	printf("\nExit.\n");
    }
    
    char* getMessage(char* sendline, int len, FILE* fp) {
    	printf("Input string to server:");
    	return fgets(sendline, MAXDATASIZE, fp);
    }
    
    
    更多相关内容
  •   在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了...一、多进程并发服务器的概念   在 Linux 环境下多进程的应用很多,其中最主要的就是网络/客户服务器。多进程服务器是当客户有请求时,...

    一、多进程并发服务器

      在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了处理客户的请求,对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:

    • 迭代服务器:服务器在同一时刻只能响应一个客户端的请求;

    • 并发服务器:服务器在同一时刻可以响应多个客户端的请求。

    1、多进程并发服务器的概念

      在 Linux 环境下多进程的应用很多,其中最主要的就是网络/客户服务器多进程服务器是当客户有请求时,服务器用一个子进程来处理客户请求。父进程继续等待其它客户的请求。这种方法的优点是当客户有请求时,服务器能及时处理客户,特别是在客户服务器交互系统中。对于一个 TCP 服务器,客户与服务器的连接可能并不马上关闭,可能会等到客户提交某些数据后再关闭,这段时间服务器端的进程会阻塞,所以这时操作系统可能调度其它客户服务进程,这比起迭代服务器大大提高了服务性能。

    2、多进程并发服务器的流程

    #include <头文件>
    
    // 信号处理函数,回收子进程
    void sig_chld(int signo) { }
    
    int main(int argc, char const *argv[])
    {
    	lfd = socket();
    	bind();
    	listen();
    	
    	signal(); // 发送SIGCHLD信号
    
    	while(1)
    	{
    		int cfd = accept();
    		if (fork() == 0)
    		{
    			close(lfd); // 子进程关闭父进程的套接字
    			/*事务处理*/
    			close(cfd); // 子进程关闭自己的套接字
    			exit(0); // 子进程退出
    		}
    		// 父进程关闭子进程的套接字(子进程文件描述符引用计数变为0,就关闭客户端)
    		close(cfd); 
    	}	
    	close(lfd);
    	return 0;
    }
    

    (1)面试题—>为什么要多次关闭文件描述符?

      每一个socket描述符都有对应的引用计数,该计数存在文件表中。上面程序中打开了lfd和cfd,引用计数分别为1和1,在fork()以后,子进程复制了父进程的socket描述符,所以引用计数也会增加,即lfd和cfd的引用计数都变成了2。此时在子进程中关闭lfd(close(lfd)),在父进程中关闭close(cfd)。这个就保证了子进程处理与客户的连接,父进程负责在监听套接字lfd再次调用accept来接收客户的下一个连接。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
      从上面分析和图示可以看出socket描述符是有引用计数的,只有当引用计数为0的时候,close才会发送FIN报文,这就解释了子进程仍然可以用cfd进行数据处理了,子进程处理完后再次调用close,此时引用计数从1变为0,最后发送FIN报文结束cfd连接。而对于lfd,只有在子进程中进行了close,父进程中一直保留着引用计数为1,所以父进程通过for循环可以持续accept新连接

    (2)面试题—>shutdown和close的区别?

      通常调用close函数对socket进行关闭,为啥还要选用shutdown来关闭socket,原因有如下两个:

    • 在介绍close的时候,已经说明了,close只有在对应socket的引用计数为0时,才会真正发送FIN报文来关闭这个连接,shutdown没有这个限制,直接发送FIN报文

    • close同时终止了读和写两个方向的数据传输。但是TCP的双工的,我们有时候需要只接受数据,而不发送数据,shutdown可以指定关闭读端或者写端

    int shutdown(int fd, int how);
    /*
    how为SHUT_RD(关闭读端),则无法从套接字读取数据;-------不发送FIN
    how为SHUT_WR(关闭写端),则无法从套接字写数据;---------发送FIN
    how为SHUT_RDWR(关闭读写),则无法从套接字读和写数据;-------发送FIN
    */
    

    3、TCP多进程并发服务器的demo

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <arpa/inet.h>
    #include <errno.h>
    #include <pthread.h>
    #include <signal.h>
    #include <ctype.h>
    
    void sys_err(const char *str)
    {
        perror(str);
        exit(1);
    }
    
    // 信号处理函数
    void sig_chld(int signo)
    {
        pid_t pid;
        int stat; // 回收状态
        // 以非阻塞形式回收子进程
        while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
            printf("child %d terminated\n", pid);
        return;
    }
    
    int main(int argc, char **argv)
    {
        int lfd, cfd;
        pid_t pid;
        socklen_t clt_addr_len;
        struct sockaddr_in srv_addr, clt_addr;
        // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
        // memset(&srv_addr, 0, sizeof(srv_addr));
        // bzero也可以用来清零操作 
        bzero(&srv_addr, 0);
        srv_addr.sin_family = AF_INET;
        srv_addr.sin_port = htons(8000);
        srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        // 创建套接字
        lfd = socket(AF_INET, SOCK_STREAM, 0);
        
        // 端口复用
        int opt = 1;
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
    
        // 绑定套接字
        bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    
        // 监听客户端的连接
        listen(lfd, 128);
        
        clt_addr_len = sizeof(clt_addr);
        
        // 子进程无论是正常退出还是异常退出都会给父进程发送SIGCHLD信号
        signal(SIGCHLD, sig_chld);
    
        char buf[512];
        while (1)
        {
            // 阻塞接收客户端的连接
            cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
                    
            // 使用fork创建子进程
            if ( (pid = fork()) < 0)
                sys_err("fork");
            
            // 子进程
            else if (pid == 0)
            {
            // 关闭不需要的套接字可节省系统资源,
            // 同时可避免父子进程共享这些套接字,
            // 可能带来的不可预计的后果。
                close(lfd); // 关闭服务器套接字描述符,这个套接字是从父进程继承过来的
                while (1)
                {
                    // 清空数组
                    memset(buf, 0, 512); 
                    // 接收数据
                    recv(cfd, buf, sizeof(buf), 0);
                    int ret = strlen(buf);
                    if (ret == 0)
                    {
                        // 没有接收到数据说明客户端关闭了连接
                        close(cfd);
                        exit(1);
                    }
                    for (int i = 0; i < ret; ++i)
                        buf[i] = toupper(buf[i]);
                    // 回射给客户端
                    send(cfd, buf, ret, 0);
                    // 服务器把数据写到标准输出
                    write(STDOUT_FILENO, buf, ret);
                    memset(buf, 0, 512);
                    close(cfd); // 子进程关闭自己的套接字
                    exit(0); // 子进程退出
                }
            }
            // 父进程
            else
            {
                // 关闭客户端套接字描述符
                close(cfd);
                continue;
            }
        }
        close(lfd); // 最后关闭服务器套接字描述符
        return 0;
    }
    

    二、多线程并发服务器

    1、多线程并发服务器的概念

      多线程服务器是对多进程的服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建。据统计,创建线程与创建进程要快 10100 倍,所以又把线程称为“轻量级”进程。线程与进程不同的是:一个进程内的所有线程共享相同的全局内存、全局变量等信息,这种机制又带来了同步问题

    2、多线程并发服务器的流程

    #include <头文件>
    
    void *client_process(void *arg)
    {
    	int cfd = *(int *)arg; // 传过来的已连接套接字
    	/*事务处理*/
    	close(cfd);
    }
    int main(int argc, char const *argv[])
    {
    	lfd = socket();
    	bind();
    	listen();
    
    	while(1)
    	{
            cfd = accept();
            pthread_t thread_id;
            if (cfd > 0)
            {
                //给回调函数传的参数,&connfd,地址传递
                pthread_create(&thread_id, NULL, client_process, (void *)&cfd);  //创建线程
                pthread_detach(thread_id); // 线程分离,结束时自动回收资源
            }
    	}	
    	close(lfd);
    	return 0;
    }
    
    pthread_create(&thread_id, NULL, client_process, (void *)&cfd)
    

      在这里我们使用的是按地址传递,所以会有这么一个问题,假如有多个客户端要连接这个服务器,正常的情况下,一个客户端连接对应一个 cfd,相互之间独立不受影响,但是,假如多个客户端同时连接这个服务器,A 客户端的连接套接字为 cfd,服务器正在用这个 cfd 处理数据,还没有处理完,突然来了一个 B 客户端,accept()之后又生成一个 cfd, 因为是地址传递, A 客户端的连接套接字也变成 B 这个了,这样的话,服务器肯定不能再为 A 客户端服务器了,这时候,我们就需要考虑多任务的互斥或同步问题了,这里通过互斥锁来解决这个问题,确保这个cfd值被一个临时变量保存过后,才允许修改

    3、TCP多线程并发服务器的demo

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <arpa/inet.h>
    #include <errno.h>
    #include <pthread.h>
    #include <signal.h>
    #include <ctype.h>
    
    pthread_mutex_t mutex;
    
    // 信号处理函数
    void *client_process(void *arg)
    {
        char buf[1024] = "";	// 接收缓冲区
    	int cfd = *(int *)arg; // 传过来的已连接套接字
    
    	// 解锁,pthread_mutex_lock()唤醒,不阻塞
    	pthread_mutex_unlock(&mutex);
        
        // 接收数据
        recv(cfd, buf, sizeof(buf), 0);
        int ret = strlen(buf);
        if (ret == 0)
        {
            // 没有接收到数据说明客户端关闭了连接
            close(cfd);
            exit(1);
        }
        for (int i = 0; i < ret; ++i)
            buf[i] = toupper(buf[i]);
        // 回射给客户端
        send(cfd, buf, ret, 0);
        // 服务器把数据写到标准输出
        write(STDOUT_FILENO, buf, ret);
    	close(cfd); // 子进程关闭自己的套接字
        return NULL;
    }
    
    int main(int argc, char **argv)
    {
        int lfd, cfd;
        socklen_t clt_addr_len;
        struct sockaddr_in srv_addr, clt_addr;
    
        pthread_mutex_init(&mutex, NULL); // 初始化互斥锁,互斥锁默认是打开的
    
        // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
        // memset(&srv_addr, 0, sizeof(srv_addr));
        // bzero也可以用来清零操作 
        bzero(&srv_addr, 0);
        srv_addr.sin_family = AF_INET;
        srv_addr.sin_port = htons(8000);
        srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        // 创建套接字
        lfd = socket(AF_INET, SOCK_STREAM, 0);
        
        // 端口复用
        int opt = 1;
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
    
        // 绑定套接字
        bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    
        // 监听客户端的连接
        listen(lfd, 128);
        
        clt_addr_len = sizeof(clt_addr);
        
        pthread_t thread_id;
        
        while (1)
        {
            // 阻塞接收客户端的连接
            cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
            if (cfd > 0)
            {
                //给回调函数传的参数,&connfd,地址传递
                pthread_create(&thread_id, NULL, client_process, (void *)&cfd);  //创建线程
                pthread_detach(thread_id); // 线程分离,结束时自动回收资源
            }
        }
        close(lfd);
        return 0;
    }
    

    参考:https://blog.csdn.net/tennysonsky/article/details/45671215

    展开全文
  • 以下内容转自Linux并发服务器编程之多进程并发服务器。 目录前言创建进程Linux下的进程进程创建函数fork与vfork使用fork函数实现多进程并发服务器分割I/O程序扩展-进程的终止 前言 服务器按处理方式可以分为迭代...

    部分内容转自Linux并发服务器编程之多进程并发服务器

    前言

    服务器按处理方式可以分为迭代服务器和并发服务器两类。平常写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很低,通常这种服务器被称为迭代服务器。 然而在实际应用中,不可能让一个服务器长时间地为一个客户服务,而需要其具有同时处理多个客户请求的能力,这种同时可以处理多个客户请求的服务器称为并发服务器,其效率很高却实现复杂。在实际应用中,并发服务器应用的最广泛。

    在这里插入图片描述

    linux有3种实现并发服务器的方式:多进程并发服务器,多线程并发服务器,IO复用,先来看多进程并发服务器的实现。


    创建进程

    Linux下的进程

    在创建新进程时,要进行资源拷贝。Linux 有三种资源拷贝的方式:

    1. 共享:新老进程共享通用的资源。当共享资源时,两个进程共同用一个数据结构,不需要为新进程另建。
    2. 直接拷贝:将父进程的文件、文件系统、虚拟内存等结构直接拷贝到子进程中。子进程创建后,父子进程拥有相同的结构。
    3. 写时复制(Copy on Write):拷贝虚拟内存页是相当困难和耗时的工作,所以能不拷贝就最好不要拷贝,如果必须拷贝,也要尽可能地少拷贝。为此,Linux 采用了 Copy on Write技术,把真正的虚拟内存拷贝推迟到两个进程中的任一个试图写虚拟页的时候。如果某虚拟内存页上没有出现写的动作,父子进程就一直共享该页而不用拷贝。

    进程创建函数fork与vfork

    下面介绍创建新进程的两个函数:fork()和 vfork()。

    其中,fork 用于普通进程的创建,采用的是 Copy on Write 方式;而 vfork 使用完全共享的创建,新老进程共享同样的资源,完全没有拷贝。

    fork函数原型如下:

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

    函数调用失败会返回-1。fork 函数调用失败的原因主要有两个:

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

    而如果调用成功,该函数调用会在父子进程中分别返回一次。在调用进程也就是父进程中,它的返回值是新派生的子进程的 ID 号,而在子进程中它的返回值为 0。因此可以通过返回值来区别当前进程是子进程还是父进程。

    为什么在 fork 的子进程中返回的是 0,而不是父进程 id 呢?

    原因在于:没有子进程都只有一个父进程,它可以通过调用 getppid 函数来得到父进程的 ID,而对于父进程,它有很多个子进程,他没有办法通过一个函数得到各子进程的ID。如果父进程想跟踪所有子进程的ID, 它必须记住 fork 的返回值。

    vfork函数原型如下:

      #include <unistd.h>
      pid_t vfork (void)

    vfork 是完全共享的创建,新老进程共享同样的资源,完全没有拷贝。当使用 vfork()创 建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间运行。这个奇特 状态将持续直到子进程要么退出,要么调用 execve(),至此父进程才继续执行。

    可以通过下面的程序来比较 fork 和 vfork 的不同。

    #include <sys/types.h> 
    #include <unistd.h> 
    int main(void) 
    {
        pid_t   pid; 
        int status;
         if ((pid = vfork()) == 0) 
        { 
           sleep(2); 
            printf("child running.\n"); 
             printf("child sleeping.\n"); 
             sleep(5); 
            printf("child dead.\n"); 
             exit(0); 
        } 
        else if ( pid > 0) 
        { 
             printf("parent running .\n"); 
             printf("parent exit\n"); 
             exit(0); 21.     
        } 
        else
        {
             printf("fork error.\n"); 
            exit(0); 
        } 
    } 
    

    程序运行结果如下:

    child running. 
    child sleeping. 
    child dead. 
    parent running . 
    parent exit
    

    如果将 vfork 函数换成 fork 函数,该程序运行的结果如下:

    parent running . 
    parent exit 
    [root@localhost test]# 
    child running. 
    child sleeping. 
    child dead. 
    

    fork与vfork区别:

    1. fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段

    2. fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

    3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁

    4. fork当需要改变共享数据段中变量的值,则拷贝父进程(写时复制);vfork()的子进程里不能访问/修改数据。


    exec函数

    fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。

    具体见exec函数详解

    使用fork函数实现多进程并发服务器

    fork 调用后,父进程和子进程继续执行 fork 函数后的指令,是父进程先执行还是子进程先执行是不确定的,这取决于系统内核所使用的调度算法。
      
    而在网络编程中,父进程中调用 fork 之前打开的所有套接字描述符在函数 fork 返回之后都是共享。如果父、子进程同时对同一个描述符进行操作, 而且没有任何形式的同步,那么它们的输出就会相互混合

    fork函数在并发服务器中的应用:
      
    父、子进程各自执行不同的程序段,这是非常典型的网络服务器。父进程等待客户的服务请求。当这种请求到达时,父进程调用 fork 函数,产生一个子进程,由子进程对该请求作处理。父进程则继续等待下一个客户的服务请求。并且这种情况下,在 fork 函数之后,父、子进程需要关闭各自不使用的描述符,即父进程将不需要的已连接描述符关闭,而子进程关闭不需要的监听描述符。这么做的原因有3个:

    1. 节省系统资源
    2. 防止上面提到的父、子进程同时对共享描述符进程操作
    3. 最重要的一点,是确保close函数能够正确关闭套接字描述符

    我们在socket编程中调用 close 关闭已连接描述符时,其实只是将访问计数值减 1。而描述符只在访问计数为 0 时才真正关闭。所以为了正确的关闭连接,当调用 fork 函数后父进程将不需要的 已连接描述符关闭,而子进程关闭不需要的监听描述符。

    好了,有了上面的知识,我们现在可以总结出编写多进程并发服务器的基本思路

    1. 建立连接
    2. 服务器调用fork()产生新的子进程
    3. 父进程关闭连接套接字,子进程关闭监听套接字
    4. 子进程处理客户请求,父进程等待另一个客户连接。

    服务器端代码示例(来源于网络):

    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/types.h>
    #include <netdb.h>
    #define SERV_PORT 1113
    #define LISTENQ  32
    #define MAXLINE 1024
    /***连接处理函数***/
    void str_echo(int fd);
    int 
    main(int argc, char *argv[]){
      int listenfd,connfd;
      pid_t childpid;
      socklen_t clilen;
      struct sockaddr_in servaddr;
      struct sockaddr_in cliaddr;
      if((listenfd = socket(AF_INET, SOCK_STREAM,0))==-1){
         fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
         exit(1);
      }
      /* 服务器端填充 sockaddr结构*/ 
      bzero(&servaddr, sizeof(servaddr));
      servaddr.sin_family = AF_INET;
      servaddr.sin_addr.s_addr = htonl (INADDR_ANY);
      servaddr.sin_port = htons(SERV_PORT);
      /* 绑定listenfd描述符  */ 
      if(bind(listenfd,(struct sockaddr*)(&servaddr),sizeof(struct sockaddr))==-1){
        fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
        exit(1);
       }
       /* 监听listenfd描述符*/
        if(listen(listenfd,5)==-1){
            fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
            exit(1);
        }
      for ( ; ; )  {
        clilen = sizeof(cliaddr);
        /* 服务器阻塞,直到客户程序建立连接  */
        if((connfd=accept(listenfd,(struct sockaddr*)(&cliaddr),&clilen))==-1){
            fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
            exit(1);
        }
        //有客户端建立了连接后
        if ( (childpid = fork()) == 0) { /*子进程*/
         close(listenfd);    /* 关闭监听套接字*/
         str_echo(connfd);   /*处理该客户端的请求*/
         exit (0);
        }
        close(connfd);/*父进程关闭连接套接字,继续等待其他连接的到来*/
     }
    }
    void str_echo(int sockfd){
     ssize_t n;
        char  buf[MAXLINE];
        again:
          while ( (n = read(sockfd, buf, MAXLINE)) > 0)
              write(sockfd, buf, n);
          if (n < 0 && errno == EINTR)//被中断,重入
              goto again;
          else if (n < 0){//出错
            fprintf(stderr,"read error:%s\n\a",strerror(errno));
            exit(1);
          }
    }
    

    传统的网络服务器程序大都在新的连接到达时,fork一个子进程来处理。虽然这种模式很多年使用得很好,但fork有一些问题:

    1. fork是昂贵的。fork时需要复制父进程的所有资源,包括内存映象、描述字等;目前的实现使用了一种写时拷贝(copy-on-write)技术,可有效避免昂贵的复制问题,但fork仍然是昂贵的;
    2. fork子进程后,父子进程间、兄弟进程间的通信需要进程间通信IPC机制,给通信带来了困难;
    3. 多进程在一定程度上仍然不能有效地利用系统资源;
    4. 系统中进程个数也有限制。

    分割I/O程序

    如下图所示,客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样无论客户端是否从服务器端接收完数据都可以进行传输。

    在这里插入图片描述

    按照这种实现方式,父进程中只需编写接收数据的代码,子进程中只需编写发送数据的代码,可以简化程序实现。

    分割I/O程序可以提高频繁交换数据的程序性能,因为分割了之后发送数据时不用考虑接收数据的情况,可以连续发送数据,提高同一时间内传输的数据量。

    部分代码实例:

    pid=fork()
    if(pid==0)
    	write_routine(sock,buf);
    else
    	read_routine(sock,buf);
    

    扩展-进程的终止

    进程终止存在两种可能:父进程先于子进程终止(孤儿进程);子进程先于父进程终止(僵尸进程)。

    如果父进程在子进程之前终止,则所有子进程的父进程被改为 init 进程,就是由 init 进程领养。在一个进程终止是,系统会逐个检查所有活动进程,判断 这些进程是否是正要终止 的进程的子进程。如果是,则该进程的父进程 ID 就更改为 1(init 的 ID)。这就保证了每个 进程都有一个父进程。

    如果子进程在父进程之前终止,系统内核会为每个终止子进程保存一些信息,这样父进 程就可以通过调用 wait()或 waitpid()函数,获得子进程的终止信息。终止子进程保存的信息 包括进程 ID、该进程的终止状态,以及该进程使用的 CPU 时间总量。当父进程调用 wait() 或 waitpid()函数时,系统内核可以释放终止进程所使用的所有存储空间,关闭其所有打开文 件。一个已经终止,但是其父进程尚未对其进行善后处理的进程称为僵尸进程。

    当子进程正常或异常终止时,系统内核向其父进程发送 SIGCHLD 信号,默认情况下, 父进程忽略该信号,或者提供一个该信号发生时即被调用的函数。
      
    父进程可以通过调用 wait()或 waitpid()函数,获得子进程的终止信息。

    wait 函数原型如下:

    #include <sys/wait.h> 
    pid_t wait(int *statloc);
    

    参数 statloc 返回子进程的终止状态(一个整数)。当调用该函数时,如果有一个子进程 已经终止,则该函数立即返回,并释放子进程所有资源,返回值是终止子进程的 ID 号。如果当前没有终止的子进程,但有正在执行的子进程,则 wait 将阻塞直到有子进程终止时才返 回。如果当前既没有终止的子进程,也没有正在执行的子进程,则返回错误-1。

    函数 waitpid 对等待哪个进程终止及是否采用阻塞操作方式方面给了更多的控制。

    waitpid函数原型如下:

    #include <sys/wait.h> 
    waitpid(pid_t pid ,int *statloc, int option);
    

    当参数 pid 等于-1 而 option 等于 0 时,该函数等同于 wait()函数。 参数 pid 指定了父进程要求知道哪些子进程的状态,当 pid 取-1 时,要求知道任何一个子进程的终止状态。当 pid 取值大于 0 时,要求知道进程号为 pid 的子进程的终止状态。当 pid 取值小于-1 时,要求知道进程组号为 pid 的绝对值的子进程的终止状态。

    参数 option 让用户指定附加选项。最常用的选项是 WNO_HANG,它通知内核在没有已 终止子进程时不要阻塞。

    当前有终止的子进程时,返回值为子进程的 ID 号,同时参数 statloc 返回子进程的终止 状态。否则返回值为-1。

    和wait较大的不同是waitpid可以循环调用,等待所有任意进程结束,而wait只有一次机会。

    展开全文
  • (一)多进程并发执行是使用后台任务来实现任务的“多进程化”。在不加控制的模式下,不管有多少任务,全部都后台执行。也就是说,在这种情况下,有多少任务就有多少“进程”在同时执行。 (二)实验中要用到的函数 ...
  • 1)服务器端代码main函数#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; liste....

    多进程并发模型

    相比同步阻塞迭代模型,多进程并发模型可以避免是程序阻塞在read系统调用上。如果没有客户端来建立连接,则会阻塞在accept处。一旦某个客户端连接建立起来,则立即开启一个新的进程来处理与这个客户的数据交互。避免程序阻塞在read调用,而影响其他客户端的连接。

    缺陷:

    在多进程并发模型中,每一个客户端连接开启fork一个进程,虽然linux中引入了写实拷贝机制,大大降低了fork一个子进程的消耗,但若客户端连接较大,则系统依然将不堪负重。


    代码:《UNIX网络编程卷1:套接字联网API》

    实现功能:

    代码运行需要配置UNIX网络编程的环境,可参考https://blog.csdn.net/damage233/article/details/81004680

    服务器

    客户端




    1)服务器端代码

    main函数
    #include	"unp.h"
    
    int
    main(int argc, char **argv)
    {
    	int					listenfd, connfd;
    	pid_t				childpid;
    	socklen_t			clilen;
    	struct sockaddr_in	cliaddr, servaddr;
    
    	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    
    	bzero(&servaddr, sizeof(servaddr));
    	servaddr.sin_family      = AF_INET;
    	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    	servaddr.sin_port        = htons(SERV_PORT);
    
    	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    
    	Listen(listenfd, LISTENQ);
    
    	for ( ; ; ) {
    		clilen = sizeof(cliaddr);
    		connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
    
    		if ( (childpid = Fork()) == 0) {	/* child process */
    			Close(listenfd);	/* close listening socket */
    			printf("%d\n",connfd);
    			str_echo(connfd);	/* process the request */
    			exit(0);
    		}
    		Close(connfd);			/* parent closes connected socket */
    	}
    }
    str_echo函数
    #include	"unp.h"
    
    void
    str_echo(int sockfd)
    {
    	ssize_t		n;
    	char		buf[MAXLINE];
    
    again:
    	while ( (n = read(sockfd, buf, MAXLINE)) > 0)
    		Writen(sockfd, buf, n);
    
    	if (n < 0 && errno == EINTR)
    		goto again;
    	else if (n < 0)
    		err_sys("str_echo: read error");
    }


    2)客户端代码

    main函数
    #include	"unp.h"
    
    int
    main(int argc, char **argv)
    {
    	int					sockfd;
    	struct sockaddr_in	servaddr;
    
    	if (argc != 2)
    		err_quit("usage: tcpcli <IPaddress>");
    
    	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
    
    	bzero(&servaddr, sizeof(servaddr));
    	servaddr.sin_family = AF_INET;
    	servaddr.sin_port = htons(SERV_PORT);
    	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    
    	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
    
    	printf("%d\n",sockfd );
    
    	str_cli(stdin, sockfd);		/* do it all */
    
    	exit(0);
    }
    str_cli函数
    #include	"unp.h"
    
    void
    str_cli(FILE *fp, int sockfd)
    {
    	char	sendline[MAXLINE], recvline[MAXLINE];
    
    	while (Fgets(sendline, MAXLINE, fp) != NULL) {
    
    		Writen(sockfd, sendline, strlen(sendline));
    
    		if (Readline(sockfd, recvline, MAXLINE) == 0)
    			err_quit("str_cli: server terminated prematurely");
    
    		Fputs(recvline, stdout);
    	}
    }


    展开全文
  • 多进程&多路复用并发的http服务器,侧重点在于仿造nignix对并发的处理机制
  • 多进程并发服务器

    千次阅读 2017-03-23 16:40:56
    我们来考虑有个客户同时连接一个服务器的情况。在前面的TCP套接字编程的例子中,我们已经看到,服务器程序在接受来自客户端的一个新连接时,会创建出一个新的套接字(已连接套接字),而原先的监听套接字则继续...
  • https://blog.csdn.net/apollon_krj/article/details/58174652 同类基础博客: 基于Linux的SOCKET编程之TCP半双工Client-Server聊天程序 基于Linux的Socket编程之TCP全双工Server-Client聊天程序 一、多进程并发分析...
  • 目前Linux平台的Web服务器主要基于进程或线程机制,面对大数量的并发请求,延时现象较为明显。这主要原因在于服务器存在着利用率不高,资源消耗大等问题。文中利用信号量机制和生产者一消费者模型,设计基于线程池...
  • 多进程并发服务器 父进程监听,子进程领任务做具体逻辑的实现。 父进程关闭cfd,close(cfd),子进程关闭lfd,close(lfd),因为父子进程共享文件描述符表。 子进程回收机制,通过信号捕捉函数while(1){waitpid()...}...
  • 不熟悉socket编程的小伙伴可以看我之前的文章,但是当时所实现的功能服务器同时只能和一个客户端进行交互,效率太低,利用多进程或者多线程方式来实现服务器可以做到同时和多个客户端进行交互。提高服务器的性能。 ...
  • 简单介绍:nginx 采用的是多进程(单线程) + io多路复用(epoll)模型 实现高并发 二、nginx 多进程 启动nginx 解析初始化配置文件后会 创建(fork)一个master进程 之后 这个进程会退出 master 进程会 变为...
  • 一:服务器模型一般分为两种1:循环服务器:服务器同一时刻只能响应一个客户端的请求2:并发服务器:服务器在同一时刻可以响应多个客户端的请求二:并发服务器的三中实现方式1:多进程并发服务器是指TCP连接后,每一...
  • 然而在实际应用中,不可能让一个服务器长时间地为一个客户服务,而需要其具有同时处理 个客户请求的能力,这种同时可以处理个客户请求的服务器称为并发服务器,其效率很 高却实现复杂。在实际应用中,并发服
  • linux---高并发服务器(网络)

    千次阅读 2021-12-11 10:11:27
    多进程并发服务器 为提高服务器效率,服务器应能同时被多个客户端进程使用,且能处理多个用户请求,实际上,我们在生活、应用中接触到的服务器,都能实现并发功能。 在多进程并发服务器中,若有用户请求到达,服务器...
  • 35-并发服务器多进程

    千次阅读 2017-04-20 17:08:11
    待你进一步完善了前面的 echo 服务器后,也处理了对端发送而来的 RST 段而导致的错误。现在,我们遇到了一个新问题,即客户端在关闭退出后,服务器也关闭退出了。1. 让服务器永远运行解决的办法很简单,我们将 ...
  • 网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别? 答案一: 1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。 2,线程:相对与进程而言,线程是一个更加接近与执行体...
  • 服务器并发处理能力一台服务器在单位时间内能处理的请求越服务器的能力越高,也就是服务器并发处理能力越高。有什么方法衡量服务器并发处理能力1、吞吐率吞吐率,单位时间离服务器处理的最大请求数,单位是req...
  • 并发服务器--多进程实现

    千次阅读 2018-03-01 21:58:17
    通过简单的socket可以实现一对一的c/s通信,当个客户端同时进行服务器访问,那么服务器只能按序的一一进行...那么要想实现一个服务器能同时接受个客户端访问并且能够双工通信的并发服务器,其中一种实现方式---...
  • 高并发服务器编程之多进程并发服务器 一、多线程服务器分析:多进程并发与多线程并发实现过程差不多,只是多线程的同步、资源回收与多进程还是有很多区别的。多进程不需要记录子进程的信息,而多线程需要记录。 ...
  • 网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别? 答案一: 1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。 2,线程:相对与进程而言,线程是一个更加接近与执行体的...
  • 并发 基本概念 ...PHP并发模型可以分为多进程模式和多线程模式,具体使用的是哪一种,得看PHP使用的是哪个SAPI(服务器应用程序编程接口)。例如Apache中可能采用多进程模型,也可能采用多线程模型。 Ngi
  • 多进程编程 一、服务器并发访问的问题        服务器按处理方式可以分为迭代服务器和并发服务器两类。平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的...
  • 线程并发服务器

    千次阅读 2017-03-25 15:45:23
    多进程并发服务器的应用程序中,父进程accept一个连接,fork一个子进程,该子进程负责处理与该连接对端的客户之间的通信。 尽管多进程的编程模型中,各进程拥有独立的地址空间,减少了出错的概率,然而,fork调用...
  • 多进程并发阻塞利用进程把客户端和服务器进行管理,当有新的客户端连接到服务器时,就创建一个新的进程来管理,通过操作系统的调度,从而实现了并发的操作多线程并发阻塞多线程和多进程类似,只是线程间共享内...
  • 网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别? 答案一: 1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。 2,线程:相对与进程而言,线程是一个更加接近与执行体...
  • 如果实现一客户端与服务器的交互比较容易(直接用TCP的编程流程就可以实现,只是这样写出的程序只能是一个客户端交互释放连接后其他客户端才可以与服务器交互 ),但是要实现个客户端同时与同一服务器的交互就相对...
  • 并发服务器架构

    千次阅读 2022-01-26 17:46:58
    1、高性能(High Preormance):是指对大量的并发请求,能做出快速的响应,这就要求我们的服务器能够最大程度发挥机器的性能,使机器在满负荷的情况下,尽可能的处理并发请求,并且能及时快速的做出响应。...
  • Apache的WINDOWS版本是传统的Leader-Follow多进程模型,Nginx则 是多线程select模型(玩具?) * 配置文件采用JSON标准格式,简洁易写,而且支持行注释和块注释。Apache配置格式比较复杂,Nginx配置 格式多变怪异...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 161,805
精华内容 64,722
热门标签
关键字:

多进程并发服务器的工作机制