Linux系统编程 day05 进程控制

Linux系统编程 day05 进程控制

  • 1. 进程相关概念
  • 2. 创建进程
  • 3. exec函数族
  • 4. 进程回收

1. 进程相关概念

程序就是编译好的二进制文件,在磁盘上,占用磁盘空间。程序是一个静态的概念。进程即使启动了的程序,进程会占用系统资源,如内存、CPU等,是一个动态的概念。

在一个时间段内,如果在同一个CPU上运行了多个程序,这就叫并发。在同一个时刻,如果CPU中运行了两个以及两个以上的程序,就叫并行,并行要求计算机要有多核CPU。

计算机的每一个进程中都有一个进程控制块(PCB)来维护进程的相关信息,Linux中的进程控制块是task_struct结构体。每一个进程都有一个唯一的ID,在C语言中用pid_t表示一个非负整数。每一个进程都有自己的状态,进程的状态有创建态、就绪态、运行态、挂起态、终止态等。在CPU发生进程切换的时候,PCB需要保存和恢复一些CPU寄存器的信息。

创建态就是进程刚创建的一个状态,随后会进入就绪态,一般常将就绪态和创建态结合着看。当就绪态的进程得到CPU的执行权分得时间片的时候,就会进入运行态,当时间片消耗完则会继续进入就绪态。在运行态的进程,如果遇到了sleep命令等就会进入挂起态,当sleep结束之后就会继续进入就绪态。就绪态的进程也会因为受到SIGSTOP信号而进入到挂起态。就绪态、运行态、挂起态三个状态的进程都有可能随时进入终止态,结束程序的运行。需要值得注意的是挂起态不能直接转到运行态,必须先转为就绪态。

在这里插入图片描述

2. 创建进程

在Linux中创建子进程我们使用fork函数。该函数的原型为:

       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void); // 创建一个子进程

其中该函数需要sys/types.hunistd.h这两个头文件。该函数不需要任何参数,返回值为子进程的pid,若失败了则会返回-1。经过该函数之后,我们可以得到两个pid,不是因为fork的返回值为两个,而是有两个进程在调用fork函数。因为我们创建了一个子进程,而本来就有一个进程。父进程调用fork函数会返回子进程的pid,而子进程调用fork函数返回的是0。所以在我们可以通过判断fork函数的返回值来确定究竟是子进程还是父进程。若pid小于0,则表明子进程创建失败,若pid等于0则说明该进程是子进程,若pid大于0则说明是父进程(返回的是子进程的pid)。

在创建的子进程的时候,操作系统会拷贝一份父进程的内存,内存分为内核区和用户区,其中用户区的数据内容是完全一样的,而内核区的内容不完全一样。比如pid就在内核区,因为每个进程都使用pid作为进程的唯一标识,所以不能一样。

在这里插入图片描述
在子进程创建了之后,父进程执行到了什么位置,子进程就会继续从该位置继续执行。两者的执行顺序并不一定是父进程就优先比子进程执行,也不是子进程一定优先比父进程执行,而是谁先抢到CPU的时间片谁就优先执行。

在这里插入图片描述

如何获得该进程的进程pid呢?操作系统为我们提供了两个函数。

       #include <sys/types.h>
       #include <unistd.h>
			
       pid_t getpid(void); // 获取当前运行进程的pid
       pid_t getppid(void); // 获取当前进程的父进程的pid

这两个函数第一个函数getpid是用于获取当前运行的进程的pid。而getppid是用于获取当前运行的进程的父进程的pid。

接下来我们来看一个使用fork函数的例子。

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

int main()
{
	printf("Before fork, pid: [%d]\n", getpid());
    
	//pid_t fork(void);
	// 创建子进程
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		// 子进程
		printf("child: pid: [%d], fpid: [%d]\n", getpid(), getppid());
	}
	else
	{
		// 父进程
		sleep(2);
		printf("father: pid: [%d], fpid: [%d]\n", getpid(), getppid());
	}

	printf("After fork, pid: [%d]\n", getpid());
	return 0;
}

前面我们说了在创建子进程的时候会拷贝一份父进程的内存,那么它们共享全局变量这些吗?实际上在多进程的程序中,它们做的是读时共享,写时复制。意思就是在不对内存的数据进行修改的时候它们是共享的,但是当你修改数据的时候操作系统会复制一份新的内存映射回去,再对这块复制的内存进行修改操作。所以父子进程不能共享全局变量。下面程序就验证了这个特性。

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

int g_var = 100;

int main()
{
	// 创建子进程
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		// 父进程
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		g_var ++;
		printf("father: g_var = [%d], addr = [%p]\n", g_var, &g_var);
	}
	else
	{
		// 子进程
		sleep(1); // 避免父进程还没有执行子进程就已经结束
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		printf("child: g_avr = [%d], addr = [%p]\n", g_var, &g_var);
	}
	return 0;
}

现在有一个问题,假如现在我要创建n个子进程,又应该怎么创建呢?假如是3个,那么我能否使用以下的语句进行创建呢?

for(int i = 0; i < 3; i ++)
{
	pid_t pid = fork();
}

从代码的表面上来看,的确是创建了3个子进程,实际上这里创建的远远不止3个子进程。分析以下原因是因为每一个子进程都会去执行fork函数。假如我们把父进程记为p0,当i=0时,p0会创建一个子进程p1。当i=1时,p0会创建子进程p2,p1会创建它的子进程p3。当i=2时,p0、p1、p2、p3都会分别创建一个子进程。综上,我们可以得出这里一共创建了7个子进程。也就是循环n次就会创建 2 n − 1 2^n-1 2n1个子进程。那么又如何该完成我们创建n个进程的任务呢?用循环是肯定的,但是我们在每次创建的时候都可以让子进程跳出循环,避免子进程创建新的子进程,让父进程一直循环创建。也即是在创建子进程之后,我们需要在子进程的运行代码中使用break语句。例如创建4个子进程,代码如下。

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

// 循环创建n个进程 
int main()
{
	int i = 0;
	for(i = 0; i < 4; i ++)
	{
		pid_t pid = fork();
		if(pid < 0)
		{
			// 创建失败
			perror("fork error");
			return -1;
		}
		else if(pid > 0)
		{
			// 父进程
			printf("father--pid:[%d]--fpid:[%d]\n", getpid(), getppid());
		}
		else 
		{
			// 子进程
			printf("child--pid:[%d]--fpid:[%d]\n", getpid(), getppid());
			break;
		}
	}

	if(i != 4)
	{
		printf("[%d]--pid:[%d]--fpid:[%d]---child\n", i, getpid(), getppid());
	}
	else
	{
		printf("[%d]--pid:[%d]--fpid:[%d]---father\n", i, getpid(), getppid());
	}

	sleep(4);
	return 0;
}

在Linux的shell中,我们常用ps去查看当前还在运行的进程,以及用kill去杀死某个进程。在ps中,常用的参数由以下四个。

参数作用
-a当前系统的所有用户进程
-e当前系统的所有进程,作用与-a一样
-f按照完整格式列表显示
-u查看进程所有者以及其它一些信息
-x显示没有控制终端的进程,也就是不能与用户进行交互的进程
-j列出与作业控制相关的信息

kill中,我们会使用-9或者-15去杀死某个进程,这里的数字是一些信号。在Linux中的信号有以下:

在这里插入图片描述

3. exec函数族

有时候我们需要一个进程里面去执行其它的命令或者是用户的自定义程序,这个时候就需要我们使用exec函数族中的函数。使用的一般方法都是先在父进程中创建子进程,然后在子进程中调用exec函数。exec函数族的常用函数原型如下:

       #include <unistd.h>

       int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
       int execlp(const char *file, const char *arg, .../* (char  *) NULL */);

这些函数的作用是在子进程中在执行自定义应用程序或者命令。

函数名参数返回值作用
execlpathname:文件路径名
arg:占位参数
…:程序的外部参数
成功不返回,失败返回-1并设置errno在子进程中执行路径pathname指定的程序
execlpfile:文件名
arg:占位参数
…:程序的外部参数
成功不返回,失败返回-1并设置errno在子进程中执行file文件

上面两个exec函数中的第二个参数arg是占位参数,一般写成和第一个参数一样的,这个参数的作用在于使用ps查询进程的时候可以看到进程名为arg的值。后面的...为执行的外部参数,比如我们使用ls命令的时候需要按照时间顺序逆序排序则需要写成-ltr。在...写完之后,必须写上一个NULL表示参数结束。

一般我们使用execl函数来执行自定义应用程序,而使用execlp来执行内部的命令。使用execlp的时候,第一个file参数会根据系统的PATH变量的值来进行搜索。

使用execl的示例如下:

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

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		execl("helloworld", "helloworld", NULL);
	}
	else if(pid > 0)
	{
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(20);
	}
	return 0;
}

使用execlp的示例如下:

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

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid == 0)
	{
		sleep(4);
		printf("child --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());
		execlp("ls", "ls", "-ltr", NULL);
	}
	else
	{
		sleep(10);
		printf("father --- pid = [%d] --- fpid = [%d]\n", getpid(), getppid());
	}
	return 0;
}

4. 进程回收

当一个进程退出之后,进程能够回收自己用户区的资源,但是不能回收内核空间的PCB资源,这个必须要它的父进程调用wait或者是waitpid函数完成对子进程的回收,避免造成系统资源的浪费。这两个函数在后面会进行介绍。

在一个程序中,如果父进程已经死了,而子进程还活着,那么这个子进程就成为了孤儿进程。为了保证每一个进程都有一个父进程,孤儿进程会被init进程领养,init进程就会成为子进程的养父进程,当孤儿进程退出之后,由init程序完成对孤儿进程的回收。需要注意的是在某些使用Systemd来管理系统的Ubuntu上就可能是由systemd进程来收养,而不是init进程。如下面就是一个孤儿进程的案例。

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


int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		sleep(3);
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	}
	else
	{
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(10);
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	}

	return 0;
}

如果在一个程序中,子进程已经死了,而父进程还活着,但是父进程没有用wait或者waitpid对子进程进行回收,则这个子进程就会称为僵尸进程。

在这里插入图片描述僵尸进程在用ps进行查询的时候会有<defunct>标识。由于僵尸进程是一个已经死亡了的进程,所以我们不能使用kill进行杀死,那么怎么解决僵尸进程的问题呢?

第一个解决方法是将它的父进程给杀死,因为父进程死亡之后僵尸进程会被init进程所领养,然后被init进程回收其资源。第二个方法就是在父进程中调用wait或者waitpid函数进行回收子进程的资源。这两个函数的原型如下:

       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *wstatus);
       pid_t waitpid(pid_t pid, int *wstatus, int options);

函数名参数返回值作用
waitwstatus:子进程的退出状态成功返回清理掉的子进程pid,失败返回-1(没有子进程)阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因
waipidpid:需要回收的进程pid
wstatus:子进程的退出状态
option:阻塞或者非阻塞,设置WNOHANG为非阻塞,设置为0表示阻塞
返回值大于0表示回收掉的子进程的pid,返回值为-1表示没有子进程,返回值为0且option为WNOHANG的时候表示子进程正在运行阻塞并等待子进程的退出,回收子进程残留的资源,获取子进程结束的状态退出原因

在上面的waitpid函数中,若pid=-1表示等待任一子进程;若pid>0表示等待其进程ID与pid相等的子进程;若pid=0表示等待进程组ID与当前进程相同的任何子进程;若pid<-1表示等待其组ID等于pid的绝对值的任一子进程(适用于子进程在其它组的情况)。

若我们不关心子进程的返回状态以及返回值,则可以将wstatus传为NULLwstatus的操作内容比较多,下面介绍两个常用的。

操作作用
WIFEXITED(wstatus)为非0表示程序正常结束
WEXITSTATUS(wstatus)获取进程的退出状态也就是返回值
WIFSIGNALED(wstatus)为非0表示程序异常终止
WTERMSIG(wstatus)获取进程终止的信号编号

下面给出一个这两个函数的使用案例。使用wait回收子程序资源的例子:

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

int main()
{
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		return -1;
	}
	else if(pid > 0)
	{
		// 父进程
		printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		int wstatus;
		pid_t wpid = wait(&wstatus);
		if(wpid < 0)
		{
			printf("There are no child processes to reclaim\n");
		}
		else 
		{
			if(WIFEXITED(wstatus))
			{
				// 正常退出
				printf("The process terminated normally, return = [%d]\n", WEXITSTATUS(wstatus));
				printf("Reclaim to child process wpid = [%d]\n", wpid);
			}
			else if(WTERMSIG(wstatus))
			{
				// 被信号杀死
				printf("The process is killed by signal, signal is [%d]\n", WTERMSIG(wstatus));
				printf("Reclaim to child process wpid = [%d]\n", wpid);
			}
		}
	}
	else
	{
		// 子进程
		printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
		sleep(15);
		return 100;
	}
	return 0;
}

使用waitpid的示例代码如下:

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

int main()
{
	srand(time(NULL));
	printf("father: pid = [%d], fpid = [%d]\n", getpid(), getppid());
	pid_t f_pid = getpid(); // 父亲的pid
	for(int i = 0; i < 4; i ++)
	{
		sleep(rand() % 2);
		pid_t pid = fork();
		if(pid < 0)
		{
			perror("fork error");
			return -1;
		}
		else if(pid == 0)
		{
			printf("child: pid = [%d], fpid = [%d]\n", getpid(), getppid());
			break;
		}
	}

	if(getpid() == f_pid)
	{
		// 父进程
		pid_t wpid = 0;
		int wstatus = 0;
		// 等待任意一个子进程,非阻塞
		while((wpid = waitpid(-1, &wstatus, WNOHANG)) != -1)
		{
			// 有进程死亡
			if(wpid > 0)
			{
				if(WIFEXITED(wstatus))
				{
					// 正常死亡
					printf("The process [%d] terminated normally, return = [%d]\n", wpid, WEXITSTATUS(wstatus));
				}
				else if(WIFSIGNALED(wstatus))
				{
					// 信号杀死
					printf("The process [%d] is killed by signal [%d]\n", wpid, WTERMSIG(wstatus));
				}
			}
		}
	}
	else
	{
		// 子进程
		int s_time = rand() % 10 + 10;
		int r_number = rand() % 10;
		printf("The process [%d], the father is %d, return [%d], sleep time [%d]s\n", getpid(), getppid(), r_number, s_time);
		sleep(s_time);
		return r_number;
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/190734.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【服务器能干什么】二十分钟搭建一个属于自己的 RSS 服务

如果大家不想自己捣鼓,只是想尝尝鲜,可以在下面留言,我后台帮大家开几个账号玩一玩。 哔哩哔哩【高清版本可以点击去吐槽到 B 站观看】:【VPS服务器到底能干啥】信息爆炸的年代,如何甄别出优质的内容?你可能需要自建一个RSS服务!_哔哩哔哩_bilibili 前言 RSS 服务 市…

MySQL表连接

文章目录 MySQL内外连接1.内连接2.外连接&#xff08;1&#xff09;左外连接&#xff08;2)右外连接 3.简单案例 MySQL内外连接 1.内连接 内连接的SQL如下&#xff1a; SELECT ... FROM t1 INNER JOIN t2 ON 连接条件 [INNER JOIN t3 ON 连接条件] ... AND 其他条件;说明一下…

2.5 逆矩阵

一、逆矩阵的注释 假设 A A A 是一个方阵&#xff0c;其逆矩阵 A − 1 A^{-1} A−1 与它的大小相同&#xff0c; A − 1 A I A^{-1}AI A−1AI。 A A A 与 A − 1 A^{-1} A−1 会做相反的事情。它们的乘积是单位矩阵 —— 对向量无影响&#xff0c;所以 A − 1 A x x A^{…

QT基础开发笔记

用VS 写QT &#xff0c;设置exe图标的方法&#xff1a; 选定工程--》右键--》添加---》资源--》 QString 字符串用法总结说明 Qt QString 增、删、改、查、格式化等常用方法总结_qstring 格式化-CSDN博客 总结来说&#xff1a; QString 的 remove有两种用法&#xff0c;&am…

【C++】类和对象(下篇)

这里是目录 构造函数&#xff08;续&#xff09;构造函数体赋值初始化列表 explicit关键字隐式类型转换 static成员友元友元函数友元类 内部类匿名对象匿名对象的作用const引用匿名对象 构造函数&#xff08;续&#xff09; 构造函数体赋值 在创建对象时&#xff0c;编译器通…

02、Tensorflow实现手写数字识别(数字0-9)

02、Tensorflow实现手写数字识别&#xff08;数字0-9&#xff09; 开始学习机器学习啦&#xff0c;已经把吴恩达的课全部刷完了&#xff0c;现在开始熟悉一下复现代码。对这个手写数字实部比较感兴趣&#xff0c;作为入门的素材非常合适。 基于Tensorflow 2.10.0与pycharm 1…

SASS的导入文件详细教程

文章目录 前言导入SASS文件使用SASS部分文件默认变量值嵌套导入原生的CSS导入后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;Sass和Less &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努…

电子学会C/C++编程等级考试2022年12月(二级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:数组逆序重放 将一个数组中的值按逆序重新存放。例如,原来的顺序为8,6,5,4,1。要求改为1,4,5,6,8。输入 输入为两行:第一行数组中元素的个数n(1输出 输出为一行:输出逆序后数组的整数,每两个整数之间用空格分隔。样例输入 …

Linux:docker基础操作(3)

docker的介绍 Linux&#xff1a;Docker的介绍&#xff08;1&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/134146721?spm1001.2014.3001.5502 通过yum安装docker Linux&#xff1a;Docker-yum安装&#xff08;2&#xff09;-CSDN博客https://blog.…

Mac 最佳使用指南

官方 Mac 使用手册如何在macOS系统安装根证书mac Terminal config proxy 【mac 终端配置代理】iPhone 安装 iOS 17公测版&#xff08;Public Beta)macOS 最佳命令行客户端&#xff1a;iTermMac 配置与 Linux 互信Mac mini 外接移动硬盘无法写入或者无法显示的解决方法如何在 ma…

2016年五一杯数学建模B题能源总量控制下的城市工业企业协调发展问题解题全过程文档及程序

2016年五一杯数学建模 B题 能源总量控制下的城市工业企业协调发展问题 原题再现 能源是国民经济的重要物质基础,是工业企业发展的动力&#xff0c;但是过度的能源消耗&#xff0c;会破坏资源和环境&#xff0c;不利于经济的可持续发展。目前我国正处于经济转型的关键时期&…

牛客网刷题笔记四 链表节点k个一组翻转

NC50 链表中的节点每k个一组翻转 题目&#xff1a; 思路&#xff1a; 这种题目比较习惯现在草稿本涂涂画画链表处理过程。整体思路是赋值新的链表&#xff0c;用游离指针遍历原始链表进行翻转操作&#xff0c;当游离个数等于k时&#xff0c;就将翻转后的链表接到新的链表后&am…

线性模型加上正则化

使用弹性网络回归&#xff08;Elastic Net Regression&#xff09;算法来预测波士顿房屋价格。弹性网络回归是一种结合了L1和L2正则化惩罚的线性回归模型&#xff0c;能够处理高维数据和具有多重共线性的特征。弹性网络回归的目标函数包括数据拟合损失和正则化项&#xff1a; m…

前端学习--React(4)路由

一、认识ReactRouter 一个路径path对应一个组件component&#xff0c;当我们在浏览器中访问一个path&#xff0c;对应的组件会在页面进行渲染 创建路由项目 // 创建项目 npx create router-demo// 安装路由依赖包 npm i react-router-dom// 启动项目 npm run start 简单的路…

一文读懂MySQL基础与进阶

Mysql基础与进阶 Part1 基础操作 数据库操作 在MySQL中&#xff0c;您可以使用一些基本的命令来创建和删除数据库。以下是这些操作的示例&#xff1a; 创建数据库&#xff1a; 要创建一个新的数据库&#xff0c;您可以使用CREATE DATABASE命令。以下是示例&#xff1a; CREA…

C++ day36 贪心算法 无重叠区间 划分字母区间 合并区间

题目1&#xff1a;435 无重叠区间 题目链接&#xff1a;无重叠区间 对题目的理解 移除数组中的元素&#xff0c;使得区间互不重叠&#xff0c;保证移除的元素数量最少&#xff0c;数组中至少包含一个元素 贪心算法 局部最优&#xff0c;使得重叠区间的个数最大&#xff0c…

MyBatis Generator使用总结

MyBatis Generator使用总结 介绍具体使用数据准备插件引入配置条件构建讲解demo地址 介绍 MyBatis Generator &#xff08;MBG&#xff09; 是 MyBatis 的代码生成器。它能够根据数据库表&#xff0c;自动生成 java 实体类、dao 层接口&#xff08;mapper 接口&#xff09;及m…

【STM32单片机】自动售货机控制系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用STM32F103C8T6单片机控制器&#xff0c;使用OLED显示模块、矩阵按键模块、LED和蜂鸣器、继电器模块等。 主要功能&#xff1a; 系统运行后&#xff0c;OLED显示系统初始界面&#xff0c;可通过…

Java王者荣耀

第一步是创建项目 项目名自拟 第二部创建个包名 来规范class 然后是创建类 GameFrame 运行类 package com.sxt;import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; im…

网络和Linux网络_5(应用层)HTTP协议(方法+报头+状态码)

目录 1. HTTP协议介绍 1.1 URL介绍 1.2 urlencode和urldecode 1.3 HTTP协议格式 1.4 HTTP的方法和报头和状态码 2. 代码验证HTTP协议格式 HttpServer.hpp 2.2 html正式测试 Util.hpp index.html 2.3 再看HTTP方法和报头和状态码 2.3.1 方法_GET和POST等 2.3.2 报头…
最新文章