多进程相关概念与编程


在之前的学习中,掌握了socket编程,学会了如何编写一个客户端程序和服务器程序并且实现两者之间的连接,可是存在一个很大的问题,那就是我所编写的服务器最多只能服务一个客户端,而服务器并不能这样。

一.服务器并发访问

指的是一个服务器在运行的时候,可以接受多个客户端的访问。Linux下通常有三种
实现并发服务器的方式:多进程并发服务器,多线程并发服务器,多路复用。这篇博客是我对多进程学习的一个梳理,让自己有更深的印象。

1.fork()系统调用

Linux内核在启动的最后阶段会创建init进程来执行程序/sbin/init,该进程是系统运行的第一个进程,进程号为 1,称为Linux 系统的 初始化进程,该进程会创建其他子进程来启动不同写系统服务,而每个服务又可能创建不同的子进程来执行不同的程序。
所以init进程是所有其他进程的“祖先”,并且它是由Linux内核创建并以root的权限运行,并不能被杀死。Linux 中维护着一个数据结构叫做 进程表,保存当前加载在内存中的所有进程的有关信息,其中包括进程的 PID(Process ID)、进程的状态、命令字符串等,操作系统通过进程的 PID 对它们进行管理,这些 PID 是进程表的索引。
Linux下有两个基本的系统调用可以用于创建子进程:fork()vfork()。fork在英文中是"分叉"的意思。为什么取这个名字呢?
因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就”分叉”了,所以这个名字取得很形象。在我们编程的过程中,一个函数调用只有一次返回(return),但由于fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()后,需要通过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进程在运行,而如果返回值<0的话,说明fork()系统调用出错。fork 函数调用失败的原因主要有两个:

  1. 系统中已经有太多的进 程;
  2. 该实际用户 ID 的进程总数超过了系统限制。
    每个子进程只有一个父进程,并且每个进程都可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID,这样在fork()时返回0给子进程是可取的。一个进程可以创建多个子进程,这样对于父进程而言,他并没有一个API函数可以获取其子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。这也是fork()系统调用两次返回值设计的原因。使用方法为:
pit_t pid;   /* fork会返回一个 pit_t 类型的值 */
pid = fork(void);/* 创建子进程 */

fork()系统调用会创建一个新的子进程,这个子进程是父进程的一个副本。这也意味着,系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。如果需要确保让父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来自己实现。
fork()函数包含在unistd.h的头文件中。在调用该函数之后可用一个简单的函数模型来实现父子程序的运行。

if(pid < 0)
{
   dosomething;
} 
  
else if(pid > 0)   /* 返回值是子进程的进程号,这部分代码在父进程中执行 */
{
   dosomething;
}

else if(0 == pid)   /* 返回值为0说明这里有 dosomething 又子进程代码完成 */
{
   dosomething;
}        

2.子进程继承了父进程的哪些东西

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

2.环境(environment);

3.堆栈;

4.内存;

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

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

7.信号(signal)控制设定;

8.nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级,数值越小,优先级越高);

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

8.进程组号;

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

10.当前工作目录;

11.根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变);

12.文件方式创建屏蔽字(file mode creation mask (umask))(译者注:译文取自《高级编程》,指:创建新文件的缺省屏蔽字);

13.资源限制;

14.控制终端;

子进程所独有:

进程号

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

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

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

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

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

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

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

10.不继承异步输入和输出;

3.孤儿进程和僵尸进程

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

在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项可使调用者不用阻塞。 waitpid并不等待在其调用的之后的第一个终止进程,他有若干个选项,可以控制他所等待的进程。 如果一个已经终止、但其父进程尚未对其调用wait进行善后处理(获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵死进程(zombie),ps命令将僵死进程的状态打印为Z。如果子进程已经终止,并且是一个僵死进程,则wait立即返回该子进程的状态。所以,我们在编写多进程程序时,最好调用wait()或waitpid()来解决僵尸进程的问题。
如果父进程在子进程退出之前退出了,这时候子进程就变成了孤儿进程。当然每一个进程都应该有一个独一无二的父进程,init进程就是这样的一个“慈父”,Linux内核中所有的子进程在变成孤儿进程之后都会被init进程“领养”,这也意味着孤儿进程的父进程最终会变成init进程。

4.进程间的通讯方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

二.使用多进程写服务器程序

socket()创建后返回一个sockfd的文件描述符,调用bind()函数绑定地址,listen()监听套接字,while进入循环,accept()等待客户端连接,当accept接收到客户端的连接后,调用fork()函数创建子程序,然后父进程关闭clifd,子进程关闭sockfd,今后子进程就与该客户端通讯,而父进程则会回到accept状态,等待新的客户端连接,这样循环,就可以实现多个客户端访问了。

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>   
#include <sys/socket.h>
#include <netinet/in.h>
#include <getopt.h>
#include <string.h>
#include <stdlib.h>

 
void print_usage(void * program)
{
        printf("%s usage:\n",program);
	printf("-p(--port) input the port you will bind.\n");
	printf("-h(--help) print the help information.\n");
}

int main(int argc,char **argv)
{
	int                   sockfd = -1;
	int                   rv = -1;
	char                  buf[1024];
	int                   port;
	int                   ch;
        struct sockaddr_in    servaddr;
	struct sockaddr_in    cliaddr;
	socklen_t             addrlen = sizeof(servaddr);
     

	struct option         opts[] = {
		{"port",required_argument,NULL,'p'},
		{"help",required_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]);
				break;
		}
	}

	if(!port)
	{
	        print_usage(argv[0]);
		return -1;
        }

	sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		printf("Create socket failure:%s\n",strerror(errno));
		return -1;
	}
	printf("Create socket[%d] ok!\n",sockfd);

	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(port);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if((rv = bind(sockfd,(struct sockaddr *)&servaddr,addrlen)) < 0 )
	{
	        printf("Bind sockfd on port[%d] failure:%s\n",port,strerror(errno)); 
		goto cleanup;
        }
        printf("Bind on port[%d] successfully!\n",port);
	listen(sockfd,13);
	while(1)
	{
	        int           newfd;
		int           tid;

		newfd = accept(sockfd,(struct sockaddr *)&cliaddr,&addrlen);
		
		tid = fork();

		if(tid > 0)
		{
		        close(newfd);
			continue;
	        }
		if(tid == 0)
		{
			close(sockfd);
			rv = read(newfd,buf,sizeof(buf));
			printf("%s\n",buf);
			printf("I'm son[%ld],my father is [%ld]\n",getpid(),getppid());
			close(newfd);
			return -1;
		}
	}

cleanup:
	close(sockfd);

	return 0;
}

	        
发布了8 篇原创文章 · 获赞 11 · 访问量 313

猜你喜欢

转载自blog.csdn.net/weixin_45121946/article/details/104387247