多进程编程

进程是什么?

什么是进程?在操作系统原理中是这样描述的:正在运行的程序及其占用得资源(CPU、内存、系统资源等)叫做进程。站在程序员的角度来看,我们使用vim编辑生成的C文件叫做源码,源码给程序员来看的但极其不识别,这时我们需要使用编译器gcc编译生成CPU可识别的二进制可执行程序并保存在存储介质上,这时编译生成的可执行程序只能叫做程序,而不能叫做进程。而一旦我们通过命令开始运行,那正在运行这个程序及占用的资源叫做进程了。进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序。

进程空间内存布局

Linux进程内存管理的对象都是虚拟内存,每个进程先天就有0~4G的各自互不干涉的虚拟内存空间,0—3G使用户空间执行用户自己的代码,高1GB的空间是内核空间执行Linux系统调用,这里存放在整个内核党得代码和所有的内核模块,用户所看到的和接触到的都是该虚拟地址,并不是实际的物理内存地址。Linux下一个进程在内存里有三部分的数据,就是"代码段","堆栈段","数据段"。这三个部分是构成一个完整的执行程序的必要的部分。代码段:就是存放了程序代码的数据,假如机器中数个进程运行一个相同的程序,那么他们就可以使用相同的代码段。堆栈段:存放的就是子程序的返回地址,子程序的参数以及程序的局部变量和malloc()动态申请内存的地址。数据段:存放程序的全局变量,静态变量及常量的内存空间。

Linux的内存布局图

 栈  栈内存由编译器在程序编译阶段完成,进程的栈空间位于进程用户空间的顶部并且是向下增长,每个函数的每次调用都会在栈空间中开辟自己的栈空间,函数参数,局部变量,函数返回地址等都会按照先入者为栈顶的顺序压入函数栈中,函数在返回该函数的栈空间消失,所以函数中返回局部变量的地址都是非法的。

堆内存的是在程序执行过程中分配的,用于存放进程运行中被动态分配的变量,大小不固定,堆位于非初始化数据段和栈之中,而是被动态分配添加到堆上,此时堆就向高地址扩张;当利用free等函数释放内存时,被释放的内存从堆中被踢出,堆就会缩减。因为动态分配的内存并不在函数栈中,所以当使用函数返回这段内存也是不会消失。

非初始化数据段  通常将此段称为bss段,用来存放未初始化的全局变量和static静态变量。并且在程序开始执行之前,就是在main()之前,内核会将此段中的数据初始化为0或空指针。

初始化数据段  用来保存已初始化的全局变量和static静态变量。

文本段也称代码段  这是可执行文件中由CPU执行的机器指令部分,正文段是只读的,以防止程序由于意外而修改其自身的执行

Linux内存管理的基本思想就是只有在真正访问一个地址的时候才建立这个地址的物理映射,Linux C/C++语言的分配方式共有3种方式。                                                                                                1.从静态存储区域分配。好就是数据段的内存分配,这段内存在程序编译阶段就已将分配好,在程序的整个运行期间都存在,例如全局变量,static变量。

2.在栈上创建,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集,效率很高,但是系统栈中分配的内存容量有限,比如大额数组就会把栈空间撑爆导致段错误。

3.从堆上分配,亦称为动态内存分配。程序在运行的时候malloc或new申请人已多少的内存,程序员自己负责在任何时候用free或delete释放内存。此区域内存分配称之为动态内存分配。动态内存分配的生存期由我们决定,使用非常灵活,但问题也最多,比如指向某个内存快的指针取值发生了变化又没有其他的指针指向这块内存,这块内存就无法访问,发生内存泄漏

fork()系统调用

        Linux内核在启动的最后阶段会创建init进程来执行程序/sbin/init,该进程是系统运行的第一个进程,进程号为1,称为Linux系统的初始化进程,该进程会创建其他子进程来启动不同写系统服务,而每个服务有可能创建不同的子进程来执行不同的程序。所以init进程是所有其他进程"祖先",并且它是由Linux内核创建并以root的权限运行,并不能被杀死。Linux中维护着一个数据结构叫做进程表,保存当前加载在内存中的所有进程的有关信息,其中包括进程的PID(Process ID),进程的状态,命令字符串等,操作系统通过进程的PID对他们进行管理,这些PID是进程表的索引。

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

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

每个子进程都只有一个父进程,并且每个进程都可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID,这样在fork()时返回0给子进程是可取的。一个进程可以创建多个子进程,这样对于父进程而言,他并没有一个API函数可以获取子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。

进程的创建过程

vim fork.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
 
int main(int argc, char **argv)
{
    pid_t pid;
    printf("Parent process PID[%d] start running...\n", getpid() );

    pid = fork();
    if(pid < 0)
    {
         printf("fork() create child process failure: %s\n", strerror(errno));
         return -1;
    }
    else if( pid == 0 )
    {
         printf("Child process PID[%d] start running, my parent PID is [%d]\n", getpid(),
getppid());
         return 0;
    }
    else // if( pid > 0 )
    {
         printf("Parent process PID[%d] continue running, and child process PID is [%d]\n",
getpid(), pid);
         return 0;
    }
}

编译 gcc fork.c -o fork

运行./fork

Parent process PID[26765] start running...
Parent process PID[26765] continue running, and child process PID is [26766]
Child process PID[26766] start running, my parent PID is [26765]

fork()系统调用会创建一个新的子进程,这个子进程是父进程的一个脚本,这也意味着,系统在创建新的子进程后,会将父进程的文本段,数据段,堆栈段都复制一份给子进程,但子进程有独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。这时系统中出现两个基本完全相同的进程(父子进程),这两个进程的执行没有固定的先后顺序,按个进程限先执行要看系统的进程调度策略,如果需要确保父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来实现。

我们知道main()函数里面的return()会调用exit()函数,而在任何函数的任何位置如果调用exit()将会导致退出。在编程时, 任何位置的exit()函数调用都会导致本进程(程序)退出,main()函数中的return()调用也会导致子进程退出,而其他任何函数的return()都只是导致这个函数返回,,而其他任何函数中的return()都只是这个函数返回而不会导致进程退出。

多进程改写服务器程序

在了解Linux多进程编程之后,我们就可以使用多进程编程模型改写服务器的多进程实现,流程图如下

 vim socket_server_fork.c

#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>
#include <ctype.h>
/*参数解析函数的说明*/
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)/*创建socket套接字*/
 {
 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); /* listen all the IP address on this host */
 //inet_aton("192.168.0.16", &servaddr.sin_addr); /* Only listen specify IP address on this host */
 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 )
 {
 /* Parent process close client fd and goes to accept new socket client again */
 close(clifd);
 continue;
 }
 else if ( 0 == pid )
 {
 char buf[1024];
 int i;
 printf("Child process start to commuicate with socket client...\n");
 close(sockfd); /* Child process close the listen socket fd */
 while(1)
 {
 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);
 }
 /* convert letter from lowercase to uppercase */
 for(i=0; i<rv; i++)
 {
 buf[i]=toupper(buf[i]);
 }
 rv=write(clifd, buf, rv);
 if(rv < 0)
 {
 printf("Write to client by sockfd[%d] failure: %s\n", clifd,
strerror(errno));
 close(clifd);
 exit(0);
 }
 } /* Child process loop */
 } /* Child process start*/
 }
 close(sockfd);
 return 0;
}

在该程序中,父进程accept()接收到新的连接后,就调用fork()系统调用来创建子进程来处理与客户端的通信。因为子进程就会继续继承父进程处于listen状态的socket描述符(sockfd),也会继承父进程accept()返回的客户端socket文件描述符(clifd),但子进程只处理与客户端的通信,这时他会将父进程的listen的文件描述符sockfd关闭;同样父进程只处理监听的事件,所以会将clifd关闭。

此时父子进程同时运行完成不同的任务,子进程只负责跟已经建立的客户端通信,而父进程只用来监听到来的socket客户端连接。所以当有新的客户端到来时,父进程就有机会来处理新的客户连接请求了,同时每一个客户端都会创建一个子进程为其服务。

子进程使用while(1)循环让自己一直执行,他负责将客户端发过来的小写字母改成大写字母后传回去。只有当读写socket()出错或客户端断开时才退出,在退出之前都调用close()关闭相应的套接字,因为在main()函数中,所以我们可以使用return()或exit()退出进程,但不可以使用break跳出。

编译   gcc socket_server_fork.c -o socket_server

运行   ./socket_server -p 7888

运行前一节socket通信客户端就可以实现测试了

系统限制

我们实现了多个服务器的并发访问,那是不是一个服务器就可以给无线多个客户端提供服务呢?

在Linux下每种资源都有相关的软硬限制,譬如单个用户最多能创建的子进程个数有限制,同样一个进程最多能打开的文件描述符也有相应的限制,这些限制会限制服务器能够提供并发访问客户端的数量。在Linux系统下,我们可以使用下面这两个函数来获取或设置这些限制:

#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim)
参数 resource说明:
RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的
默认行为是终止进程的执行。
RLIMIT_DATA //进程数据段的最大值。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情
况下将终止进程的执行。
RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
RLIMIT_NPROC //用户可拥有的最大进程数。
RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
RLIMIT_STACK //最大的进程堆栈,以字节为单位。
rlim:描述资源软硬限制的结构体
struct rlimit {
rlim_t rlim_cur;
rlim_t rlim_max;
};

使用方法

vim rlimit.c

#include <stdio.h> 
#include <string.h>
#include <errno.h>
#include <sys/resource.h> 
 
void print_limits(char* name, int resource)
{
 struct rlimit limit; 
 if(getrlimit(resource, &limit) <0)
 { 
     printf("getrlimit for %s failure: %s\n", strerror(errno)); 
     return ;
 } 
 printf("%-15s ",name); 
 if(limit.rlim_cur == RLIM_INFINITY)
 { 
     printf("(infinite) "); 
 }
 else
 { 
     printf("%-15ld",limit.rlim_cur); 
 }
 if(limit.rlim_max == RLIM_INFINITY)
 { 
     printf("(infinite) "); 
 }else
 { 
     printf("%-15ld",limit.rlim_max); 
 } 
 printf("\n"); 
} 
int main(void)
{ 
 struct rlimit limit = {0};
 print_limits("RLIMIT_NPROC", RLIMIT_NPROC);
 print_limits("RLIMIT_DATA", RLIMIT_DATA);
 print_limits("RLIMIT_STACK", RLIMIT_STACK);
 print_limits("RLIMIT_NOFILE", RLIMIT_NOFILE);
 printf("\nAfter set RLIMIT_NOFILE:\n");
 getrlimit(RLIMIT_NOFILE, &limit );
 limit.rlim_cur = limit.rlim_max;
 setrlimit(RLIMIT_NOFILE, &limit );
 print_limits("RLIMIT_NOFILE", RLIMIT_NOFILE);
 return 0;
}

编译运行结果

RLIMIT_NPROC 7345 7345 
RLIMIT_DATA (infinite) (infinite) 
RLIMIT_STACK 8388608 (infinite) 
RLIMIT_NOFILE 1024 1048576 
After set RLIMIT_NOFILE:
RLIMIT_NOFILE 1048576 1048576

所以我们可以知道,一个服务器抛开硬件(CPU,内存,宽带)限制以外还会收到Linux系统的资源限制。所以我们想要整加Linux服务器并发访问的客户端数量,则需要在程序里调用serrlimit()函数来修改这些限制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值