多进程服务器端及进程间通信

本文详细介绍了Linux和Windows系统下查看与管理进程的方法,包括使用`ps`命令和任务管理器。深入讲解了Linux中`fork()`函数创建进程的原理,以及如何通过`wait`和`waitpid`函数避免僵尸进程。同时,讨论了信号处理,包括`signal`和`sighand`函数,以及`sigaction`的优势。最后,提到了进程间通信的基本概念,如管道、命名管道、消息队列和共享内存等。
摘要由CSDN通过智能技术生成

查看进程

Linux:使用ps命令
Windows:使用任务管理器

Linux创建进程

通过调用fork函数创建进程:

#include <unistd.h>
pid_t fork(void);
//成功时返回进程ID,失败时返回-1

fork函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句(准确地说是在fork函数返回后)。但因为通过同一个进程、复制相同的内存空间,之后的程序l流要根据fork函数地返回值加以区分。即利用fork函数地如下特点区分程序执行流程。

  • 父进程:fork函数返回子进程ID
  • 子进程:fork函数返回0
    注意:如果父进程不等待子进程的结束而先结束,则子进程不受父进程影响,换句话说就是父进程和子进程各不影响。

代码示例

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

int gval = 10;
int main(int argc,char *argv[])
{
	pid_t pid;
	int lval = 20;
	gval++,lval+=5;

	pid = fork();
	if(pid == 0)	//if Child Process
		gval += 2,lval += 2;
	else			//if Parent Process
		gval -= 2,lval -= 2;
	
	if(pid == 0)	//if Child Process
		printf("Child proc:[%d %d]\n",gval,lval);
	else			//if Parent Process
		printf("Parent proc:[%d %d]\n",gval,lval);
	return 0;
}

输出:
在这里插入图片描述

那一年, fork() 函数弄晕了多少Windows程序猿

僵尸进程

进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。

产生僵尸进程的原因

为了防止僵尸进程的产生,先解释产生僵尸进程的原因。利用如下两个示例展示调用fork函数产生子进程的终止方式。

  • 传递参数并调用exit函数。
  • main函数中执行return语句并返回值。

向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。既然如此,此僵尸进程何时被销毁呢?其实已经给出提示。
“应该向创建子进程的父进程传递子进程的exit参数值或return语句的返回值。”

如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是说,父母要负责收回自己生的孩子(也许这种描述有些不妥)。

创建僵尸进程示例

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

int main(int argc,char *argv[])
{
	pid_t pid = fork();
	
	if(pid == 0)	//if Child Process
	{
		puts("Hi,I am a child process");
	}else{
		printf("Child Process ID:%d\n",pid);
		sleep(30);	//Sleep 30 sec.
	}
	
	if(pid == 0)
	{
		puts("End child process");
	}else{
		printf("End parent process");
	}
	
	return 0;
}

销毁僵尸进程1:利用wait函数

#include <sys/wait.h>

pid_t wait(int * statloc);
//成功时返回终止的子进程ID,失败时返回-1。

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值( exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离。

  • WIFEXITED子进程正常终止时返回“真”( true )。
  • WEXITSTATUS返回子进程的返回值。

也就是说,向wait函数传递变量status的地址时,调用wait函数后应编写如下代码。

if(WIFEXITED(status))//是正常终止的吗?
	puts ( "Normal termination!");
	printf( "Child pass num: %d"WEXITSTATUS(status));1/那么返回值是多少?
}

利用wait函数销毁进程示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char * argv[])
{
	int status;
	pid_t pid = fork();
	
	if(pid == 0)
	{
			return 3;
	}
	else{
		printf("Child PID:%d\n",pid);
		pid = fork();
		if(pid == 0)
		{
			exit(7);
		}
		else{
			printf("Child PID:%d\n",pid);
			wait(&status);
			if(WIFEXITED(status))
			printf("Child send one:%d\n",WEXITSTATUS(status));
		
			wait(&status);
			if(WIFEXITED(status))
			printf("Child send two:%d\n",WEXITSTATUS(status));
			sleep(10);
		}
		
	}
	return 0;
}

注意:调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数。调用waitpid函数程序不会阻塞。

销毁僵尸进程2:使用waitpid函数

#include <sys/wait.h>

pid_t waitpid(pid_t pid,int * statloc,int options);
//成功时返回终止的子进程ID(或0),失败时返回-1。
  • pid:等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止。
  • statloc:与wait函数的statloc参数具有相同含义。
  • options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。

利用waitpid函数销毁进程示例

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char * argv[])
{
	int status;
	pid_t pid = fork();

	if(pid == 0)
	{
		sleep(15);
		return 24;
	}else{
		while(!waitpid(-1,&status,WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}
		
		if(WIFEXITED(status))
                printf("Child send %d\n",WEXITSTATUS(status));
	}
	return 0;
}

输出:
在这里插入图片描述

信号处理

用于接收子进程终止的相关事宜。

“嘿,父进程!你创建的子进程终止了!”

信号与signed函数

#include <signal.h>

void (*signal(int signo,void (*func)(int)))(int);
//为了在产生信号时调用,返回之前注册的函数指针
  • 函数名:signal
  • 参数:int signo, void (* func)(int)
  • 返回类型:参数为int型,返回void型函数指针。

调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用弟二个参数所指的函数。下面给出可以在signal函数中注册的部分特殊情况和对应的常数。

  • SIGALRM:已到通过调用alarm函数注册的时间。
  • SIGINT:输入CTRL+C。
  • SIGCHLD:子进程终止。
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩余时间

如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALRM信号。若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理。希望引起注意。

signed函数信号处理示例

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig == SIGALRM)
		puts("Time out!");
	alarm(2);
}

void keycontrol(int sig)
{
	if(sig == SIGALRM)
		puts("CTRL+C pressed");
}

int main(int argc,char *argv[])
{
	int i;
	signal(SIGALRM,timeout);
	signal(SIGINT,keycontrol);
	alarm(2);
	
	for(i = 0;i < 3;i++)
	{
		puts("wait...");
		sleep(100);
	}
	
	return 0;
}

输出:
在这里插入图片描述

进程处于睡眠状态时无法调用函数。因此,产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程。而且,进程一旦被唤醒,就不会再进人睡眠状态。即使还未到sleep函数中规定的时间也是如此。所以,上述示例运行不到10秒就会结束,连续输入CTRL+C则有可能1秒都不到。

利用sigaction函数进行信号处理

优势:signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同。
实际上现在很少使用signal函数编写程序,它只是为了保持对旧程序的兼容。

#include <signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction*oldact);
//成功时返回0,失败时返回-1。
  • signo:与signal函数相同,传递信号信息。
  • act:对应于第一个参数的信号处理函数(信号处理器)信息。
  • oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0。

声明并初始化sigaction结构体变量以调用上述函数,该结构体定义如下。

struct sigaction
void (*sa_handler)(int);sigset_t sa_mask;
int sa_flags;
}

此结构体的sa_handler成员保存信号处理函数的指针值(地址值)。sa_mask和sa_flags的所有位均初始化为0即可。这2个成员用于指定信号相关的选项和特性,而我们的目的主要是防止产生僵尸进程,故省略。

利用sigaction信号处理示例

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig == SIGALRM)
		puts("Time out!");
	alarm(2);
}

int main(int argc,char *argv[])
{
	int i;
	struct sigaction act;
	act.sa_handler = timeout;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGALRM,&act,0);
	
	alarm(2);
	
	for(i = 0; i < 3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}

输出:
在这里插入图片描述

分割TCP的I/O程序

分割TCP的I/O程序的原因有很多,但最重要的一点是,程序的实现更加简单。按照这种实现方式,父进程只需编写接收数据的代码,子进程中只需编写发送数据的代码,所以会简化。

进程通信

进程通信意味着两个不同进程间可以交换数据。
常见进程的通信方式:

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

通过管道实现进程间通信

#include <unistd.h>

int pipe(int filedes[2]);
//成功时返回0,失败时返回-1。
  • filedes[0]:通过管道接收数据时使用的文件描述符,即管道出口。
  • filedes[1]:通过管道传输数据时使用的文件描述符,即管道入口。

管道通信示例

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main(int argc,char * argv[])
{
	int fds[2];
	char str[] = "Who are you?";
	char buf[BUF_SIZE];
	pid_t pid;
	
	pipe(fds);
	pid = fork();
	if(pid == 0)
	{
		write(fds[1],str,sizeof(str));
	}else{
		read(fds[0],buf,BUF_SIZE);
		puts(buf);
	}
	return 0;
}

说明:其实可以通过一个管道进行双向通信,但是数据进入管道后成为了无主数据。也就是通过read函数先读取数据的进程将得到数据,即使该进程将数据传到了管道,可以解决的办法是:创建2个管道

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值