Linux下的TCP/IP编程----进程及多进程服务端

时间:2020-12-31 14:54:20

在之前的学习中我们的服务端同一时间只能为一个客户端提供服务,即使是将accept()函数包含在循环中,也只能是为多个客户端依次提供服务,并没有并发服务的能力,这显然是不合理的。通过多进程的使用,我们可以很便捷的实现服务端的多进程,这样就可以同时为多个客户端提供服务。


首先我们要理解程序,进程,进程ID,僵尸进程,线程的概念。

程序:广泛的说就是为了达到某一目的二规定的途径,在编程中具体的就是为了实现某一功能而编写的代码实体,是静态的。

进程:程序的一次动态执行就是一个进程,它是占用了一定内存空间,正在运行的程序。

进程ID:在操作系统中,PCB(进程控制块)是进程存在的唯一标识,而其中进程ID则用于区分进程控制块,在系统中是唯一的,每个进程都有一个唯一的进程ID用于区分。

线程:进程更小的资源集合体,它不持有系统资源,而是通过向所在进程申请资源运行,是系统进行调度的最小单位。一个进程中可以包含有多个线程。

僵尸进程:在进程执行完之后本因该销毁,但有时因为一些原因而是的其保留了下来,虽然其已经释放了绝大部分的系统资源,但是仍保留了一小部分系统资源,对于这种进程我们称之为”僵尸进程”。

僵尸进程产生的原因:在Linux中,为了使得子进程在结束之后其结束时的信息和状态可以被父进程查询到,所以设置了一个机制:即当进程运行完成之后会保留一部分的资源用于保存结束时的状态和信息(例如进程ID,返回值等),直到其父进程获取了这些信息,子进程才完全的释放了所占用的资源。由于这种机制的存在,当父进程中执行的任务比子进程繁重时(即子进程比父进程先结束)子进程就必须要等待父进程来主动的获取信息,这样就使得僵尸进程产生了。

附:僵尸进程的产生是由于子进程比父进程先结束,必要要等到父进程查询其结束信息而产生,那么自然而然的就考虑到若是父进程比子进程先结束又会发生声么呢?当子进程还没有执行完全部任务时,若是父进程已经结束运行,那么子进程会托管给系统中进程ID为1的进程(用于协助操作系统的进程),即子进程的父进程变成了进程ID为1的进程,这时候系统会自动进行信息的采集,以保证不会产生僵尸进程。


在了解了基本的概念之后,我们就可以尝试进行多进程的实践。创建进程的方式有很多,我们只了解创建多进程服务端的函数。

pid_t fork(void):复制当前进程成为新进程

成功时返回进程ID,失败时返回-1

  • 子进程中返回ID为0

  • 父进程中返回ID为子进程的进程ID

详解:通过调用fork()函数,我们可以复制当前的进程(调用fork()函数的进程)成为一个新的子进程,俩者拥有相同而又各自独立的内存空间(也就是把父进程的内存完全复制到了另一块内存中)。我们可以根据函数的返回值来区分父进程和子进程。

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

int main(int argc ,char *argv[]){

// 创建子进程
pid_t pid = fork();

//两个进程将根据PID的不同来执行不同的代码,在父进程中pid的值为子进程的进程ID,在子进程中pid的值为0
if(pid == 0){
printf("I am Child process pid %d\n ",pid);
}else{
printf("I am parent process Child Pid %d \n",pid );
sleep(30);
}

return 0;
}

对僵尸进程的解决办法:

1. 使用wait()或者是waitpid()函数:

pid_t wait(int *statloc):获取子进程的返回值

  • statloc(内存地址):当调用该函数时,若是存在已经结束的子进程,那么该子进程的返回值就保存在该地址所指的内空间中。若是没有已经结束的子进程,那么就会阻塞主进程,直到有子进程结束为止。

成功时返回终止的子进程ID,失败时返回-1

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

int main(int argc ,char *argv[]){
//用于保存状态
int status;
//fork一个子进程
pid_t pid = fork();
if(pid == 0){
return 3;
}else{
printf("Child PID :%d \n",pid);
//在父进程中在fork一个子进程
pid = fork();
if(pid == 0){
exit(7);
}else{
printf("Child PID :%d \n",pid);
//获取子进程的返回值
wait(&status);
/**
* 由于status所指向的单元中还包含其他的信息,所以要通过以下宏定义进行判断和分离
* WIFEXITED(status)------子进程正常终止时返回真
* WEXITSTATUS(status)------子进程的返回值
**/


//判断子进程是否正常结束
if(WIFEXITED(status)){
printf("Child send one : %d \n",WEXITSTATUS(status));
}

wait(&status);
if(WIFEXITED(status)){
printf("Child send one : %d \n",WEXITSTATUS(status));
}
sleep(60);

}

}
return 0;
}

pid_t waitpid(pid_t pid , int *statloc , int options)获取子进程的返回值:

  • pid(进程ID):要结束的进程ID
  • statloc(地址指针):子进程的返回值等信息存放在该参数所指向的内存地址中
  • option(配置信息):传递头文件sys/wait.h中声明的常量
#include <stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main(int argc , char * argv[]){
//用于保存状态
int status;
//fork一个子进程
pid_t pid = fork();
if(pid == 0){
sleep(15);
return 24;
}else{
while(!waitpid(pid,&status,WNOHANG)){
sleep(3);
puts("sleap 3 seconds");
}

if(WIFEXITED(status)){
printf("Child send %d \n",WEXITSTATUS(status));
}
}
return 0;
}

2. 通过信号量机制:

void (signal(int signum,void( func)(int)))(int)注册信号量产生时需要调用的函数:

  • signo(信号量常量):要注册的信号量类型

常见信号量:

信号量 对应事件
SIGALRM 到了使用alarm()函数注册的时间
SIGINT 输入Ctrl+C
SIGCHLD 子进程结束
  • (* func)(int)(函数地址指针):指向处理信号量的函数的地址

返回之前注册的函数指针。在发生信号量时会由系统唤醒正处于睡眠状态的线程(即使是sleep时间未到也会被唤醒)

#include <stdio.h>
#include<unistd.h>
#include<sys/wait.h>
/**超时处理函数**/
void time_out (int sig){
if(sig == SIGALRM){
puts("Time Out ");
alarm(2);
}

}

/**键盘输入处理函数**/

void key_control(int sig){
if(sig == SIGINT){
puts("Ctrl + C pressed");
}
}


int main(int argc , char * argv[]){
int i = 0;
//向系统注册超时处理信号
signal(SIGALRM,time_out);
//向系统注册键盘输入Ctrl+C处理信号的
signal(SIGINT,key_control);
//设置时钟
alarm(2);

for(;i<3;i++){
puts("wait.........");
sleep(100);
}
return 0;
}

int sigaction(int signo , const struct sigaction *act , struct signation *oldaction)处理系统发出的信号量

  • signo(信号量常数):传入需要监听的信号量

  • act(结构体指针):传入处理信号量的结构体的地址

  • oldaction(结构体指针):通过次参数可获得之前注册的处理信号量

的结构体地址成功时返回0,失败时返回-1

sigaction结构体详解:

struct sigaction{
void (* sa_handler) (int);//保存信号处理函数的地址
sigset_t sa_mask; //一个调用信号捕捉函数之前要加到进程信号屏蔽字中的信号集
int sa_flags;//信号处理选项
}

该结构体后边的两个字段时用于指定信号相关的选项和特性,在此只需要全部填为0即可

使用sigaction()函数:

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

/**超时处理函数**/
void time_out(int sig){
if(sig == SIGALRM){
puts("Time Out ");
}
alarm(2);
}



int main(int argc ,char *argv[]){
int i = 0;
//声明一个sigaction变量
struct sigaction act;
/**初始化act变量中的值**/
act.sa_handler = time_out;//初始化信号处理函数的地址
act.sa_flags = 0;//初始化信号处理标志
sigemptyset(&act.sa_mask);//用来将act.sa_mask信号集初始化并清空
sigaction(SIGALRM,&act,0);//调用sigaction函数

alarm(2);//设置时钟

for(;i<3;i++){
puts("wait..........");
sleep(100);
}
return 0;
}

有了这些关于进程,僵尸进程的处理的知识我们就可以自己做一个多进程的服务端,这样就可以同时为多个客户端提供服务。

多进程服务端:

#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdbool.h>

#define BUFF_SIZE 30

void error_handling(char *message);
void read_child_proc(int sig);

int main(int argc , char *argv[]){

//服务端和客户端socket
int server_socket;
int client_socket;
//服务端和客户端地址
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//用于保存进程ID
pid_t pid;
//信号量结构体变量
struct sigaction act;
//用于保存socket地址长度
socklen_t addr_size;
//用于保存字符串长度
int str_len;
//用于记录设置信号量的结果
int state;
//字符缓冲
char buff[BUFF_SIZE];
//用于控制程序的结束与否
bool is_running = true;
//检查传入的参数个数是否合法
if(argc!=2){
printf("Usage : %s <port> \n",argv[0]);
exit(1);
}

//初始化信号量机制
act.sa_handler = read_child_proc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
state = sigaction(SIGCHLD,&act,0);

//初始化socket
server_socket = socket(PF_INET,SOCK_STREAM,0);
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));

//绑定地址
if(bind(server_socket,(struct sockaddr *) &server_addr,sizeof(server_addr)) == -1){
error_handling("bind() error");
}
//设置监听
if(listen(server_socket,5) == -1){
error_handling("listen() error");
}
//通过循环来不断的提供服务
while(is_running){
addr_size = sizeof(client_addr);
client_socket = accept(server_socket,(struct sockaddr *) &client_addr,&addr_size);
//进行检查
if(client_socket == -1){
continue;
}else{
printf("new client connected...............\n");
}
//创建新的进程
pid = fork();
//检查是否创建成功
if(pid == -1){
close(client_socket);
continue;
}
//子进程运行部分,在这里进行服务端和客户端的交互
if(pid == 0){
close(server_socket);
//不断的向客户端发送读取到的数据,直到读取完毕
while((str_len = read(client_socket,buff,BUFF_SIZE)) != 0){
write(client_socket,buff,str_len);
}
//发送完毕之后关闭客户端的连接
close(client_socket);
puts("client disconnected.........");
//子进程完成任务,返回
return 0;
}else{
close(client_socket);
}
}
//彻底关闭服务端,但是由于前边的while循环是死循环,正常情况下执行不到
close(server_socket);
return 0;
}
/**子进程处理函数**/
void read_child_proc(int sig){
pid_t pid;
int status;
//在信号量处理函数中调用waitpid()函数来读取子进程的结束信息,彻底销毁子进程,同时父进程也可以根据status中的信息来对子进程的处理结果进程进一步的处理
pid = waitpid(-1,&status,WNOHANG);
printf("remove proc id : %d \n",pid);
}
/**出错处理函数**/
void error_handling(char * message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}

自此就完成了一个多进程服务端程序的建立,我们可以使用该服务端同时为多个客户端提供服务。