Linux多线程基本概念

目录

​编辑

1.什么是进程,线程,并发,并行

优点

缺点

 什么资源是线程应该私有的呢

为什么线程切换成本更低呢

3.线程控制

pthread_create

 lpthread选项

 makefile

代码实现

 ps -aL

 什么是LWP

 轻量级进程ID与进程ID之间的区别

LWP与pthread_create创建的线程之间的关系

4.线程中止,等待,分离

pthread_exit函数

pthread_cancel函数 

 线程等待

pthread_join​编辑

 线程分离 

pthread_detach

5.线程互斥

进程线程间的互斥相关背景概念

 多线程共享资源访问的不安全问题(举个抢票的例子)

互斥量的接口


今天推荐一首歌曲 

黄昏      周传雄

依然记得从你口中~

说出再见坚决如铁~

昏暗中有种烈日灼身的错觉~

黄昏的地平线~

划出一句离别~

爱情进入永夜~

开始我们的学习吧!

1.什么是进程,线程,并发,并行

进程是资源分配的最小单位,有独立的地址空间和系统资源。
线程是cpu调度,程序执行的最小单位,一个线程只属于一个进程,而一个进程可以有多个线程,多个线程共享同一个进程的资源。在多核系统下允许几个线程各自独立的在处理器上运行,操作系统提供线程就是为了方便有效地实现这种并发性。

一切进程至少都有一个执行线程

线程在进程内部运行,本质是在进程地址空间内运行

在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

多进程:同时运行QQ、微信、浏览器 , 多线程: 用浏览器同时进行浏览网页、播放视频)

并发是把cpu运行时间划分成若干个时间段,每个时间段再分配给各个线程执行, 当一个线程在运行时,其他线程处于挂起状态。

并行是同一时刻当一个cpu执行一个线程时,另一个cpu可以执行另一个线程,两个线程互不抢占cpu资源,是真正意义上的 不同线程在同一时刻同时执行

形象地解释:
进程是拥有一系列资源的集合,这些资源包括内存空间、内核对象、资源文件等等。我们将进程理解为一个工厂,工厂本身不能运作,需要有人来操作。那么这些工人就是线程,每一个工人操作自己的一台设备,这个设备就可以看成是线程的栈,他由这个工人自己使用。一个工厂里有多台设备时,如果只有一个人那么他就需要去一个个的去操作工厂里的设备,如果这些设备需要同时运行,那么这样操作效率太低。因此,工厂会多聘用几个工人,他们每个人操作自己的设备,这样效率就会大大提高。工人在操作设备时,可能两个人需要使用同一个工具,这个工具是全局的变量,因此他们可以共同访问,但是一个工人要去使用这个工具时,他会等在那里,等另一个人使用完,然后他就可以接过工具,继续干活了,这就是线程的同步。创建多个工厂就是多进程程序。工人操作的每台设备还是属于该工厂,因此线程是依附于进程的,占用进程的地址空间,线程之间也可以相互访问对方的地址,需要通过传址能实现,但是一般不会出现这样的情况,试想能有多大的机会在一个函数中访问另一个函数的的局部变量。在代码的实现中,我们可以将线程仅仅看成一函数去分析,只不过他是并发进行的。


工厂就是进程,工人就是线程,工厂所占的位置就是进程空间,工厂里的设备和工具就是数据和资源,多个工人同时工作就是多线程,几个工人要用同时使用一个工具就是线程同步。

重点

进程是资源分配的基本单位,线程是调度的基本单位


线程独有:栈,寄存器,信号屏蔽字,errno...等信息,因此各个线程各自有各自的栈区,但是堆区共用

任何一个线程都可以创建或撤销另一个线程

进程比线程安全的原因是每个进程有独立的虚拟地址空间

Linux内核中有没有真正意义上的线程呢?没有,linux用进程的PCB来模拟线程,是完全属于自己实现的一套方案!
站在CPU的角度来看,每一个PCB,都可以称之为轻量级进程,因为它只需要PCB即可,而进程承担分配的资源更多,量级更重!
Linux线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体!

2.线程的优缺点

优点


缺点

 什么资源是线程应该私有的呢

重要的两点

线程的上下文结构也必须是线程的私有资源。(寄存器)
每个线程都有自己的私有栈结构

 

为什么线程切换成本更低呢

3.线程控制

在Linux下,PCB<=其他OS内的PCB(进程控制块) 

Linux下的进程统称为,轻量级进程

pthread_create

pthread_create是创建线程的一个接口

返回值:成功返回0;   失败返回错误码

 lpthread选项

(如果在编译时不带-lpthread选项,可以看到g++报错pthread_create()函数未定义,其实就是因为链接器链接不上具体的动态库,此时就可以看出来linux内核中并没有真正意义的线程,他无法提供创建线程的接口,而只能通过第三方库libpthread.so或libpthread.a来提供创建线程的接口。)

 makefile
//编写makefile  带上 -lpthread
test:test.cpp
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f test
代码实现
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void *threadRoutine(void *args)//新线程回调在这里
{
while(true)
{
cout<<"新线程:"<<(char*)args <<" running..."<< endl;
sleep(1);
}

}

int main()
{
    pthread_t tid;  //创建线程
    pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
                                                    //进程的名字


//下面是主线程
while(true)
{
cout<<"main线程:"<<" running..."<< endl;
sleep(1);
}
}

 ps -aL

ps命令用于查看进程信息,其中-L选项用于查看轻量级进程信息
pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID
getpid() 用于获取当前进程的id,而并非某个特定轻量级进程

通过ps -aL就可以看到正在运行的线程有哪些,可以看到有两个标识符,一个是PID,一个是LWP(light weight process),所以CPU在调度那么多的PCB时,其实是以LWP作为每个PCB的标识符,以此来区分进程中的多个轻量级进程。


主线程的PID和LWP是相同的,所以从CPU调度的角度来看,如果进程内只有一个执行流,那么LWP和PID标识符对于CPU来说都是等价的,但当进程内有多个执行流时,CPU是以LWP作为标识符来调度线程,而不是以PID来进行调度。

 什么是LWP

LWP(轻量级进程)是操作系统中用于调度和管理线程的内核层面的实体。

LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化

 轻量级进程ID与进程ID之间的区别

  1. 概念:

    • 进程ID(PID)是操作系统为每个正在运行的进程分配的唯一标识符。它是在进程创建时由操作系统分配的,并在整个进程的生命周期中保持不变。
    • 轻量级进程ID(LWP ID)是在多线程操作系统中,用于标识线程或轻量级进程(也称为执行上下文)的标识符。一个进程可以包含多个轻量级进程,并且每个轻量级进程都有自己的LWP ID。
  2. 操作方式:

    • 对于PID,通常可以使用系统调用(如fork()和exec())创建新进程或操作现有进程。
    • 对于LWP ID,通常使用线程库(如pthread)创建新线程或操作现有线程。一个进程的LWP ID只在该进程内部可见,对于其他进程来说是不可见的。
  3. 调度与资源分配:

    • 操作系统通过PID进行进程调度和资源分配。进程调度通常是基于运行队列中进程的优先级和调度算法来进行决策的。
    • 在多线程操作系统中,内核会将CPU时间划分给不同的LWP。调度和资源分配是基于LWP而不是整个进程进行的。
  4. 上下文切换:

    • 在进程切换时,操作系统需要保存和恢复整个进程的上下文,这包括进程的寄存器状态、打开的文件、堆栈等。
    • 在轻量级进程切换时,只需要保存和恢复当前线程的上下文,这是因为同一进程内的线程共享同一内存空间和打开的文件。

需要注意的是,LWP ID是在多线程操作系统中使用的概念,而PID是在所有操作系统中都存在的概念。在某些操作系统中,LWP ID可能与线程ID(TID)或任务ID(TID)等概念等效或相似。

LWP与pthread_create创建的线程之间的关系

LWP(轻量级进程)与pthread_create创建的线程之间存在一种关系,可以理解为LWP是内核层面对线程的调度和管理的实体,而pthread_create创建的线程则是用户层面对线程的抽象。

具体来说,pthread_create是一个线程库函数,用于在用户空间创建一个新的线程。这个线程由操作系统内核分配一个LWP,并将其标识为一个用户线程,也称为轻量级进程。该LWP会在它所属的进程中与其他LWP共享进程的地址空间、文件描述符等资源。

LWP与pthread_create创建的线程之间的关系可以总结如下:

  1. 一个进程可以包含多个LWP,每个LWP都有一个唯一的LWP ID。
  2. 每个LWP可以与一个或多个pthread_create创建的线程相关联,这些线程共享其所属进程的资源。
  3. 每个pthread_create创建的线程有自己的线程ID,并且在用户层面上可见,可以使用线程库提供的函数进行操作和管理。

需要注意的是,LWP的创建和管理是由操作系统内核完成的,而pthread_create函数是线程库提供的接口,它在内部会使用操作系统提供的系统调用来创建和管理LWP。因此,对于用户来说,他们只需要使用pthread_create接口来创建和操作线程,而无需直接与LWP进行交互。


4.线程中止,等待,分离

线程终止总共有三种方式,分别为return,pthread_exit,pthread_cancel

1. 从线程函数 return 。这种方法对主线程不适用 , main 函数 return 相当于调用 exit
(exit是中止进程的)
2. 线程可以调用 pthread_ exit 终止自己。
3. 一个线程可以调用 pthread_ cance l 终止同一进程中的另一个线程。
pthread_exit函数

那个线程调用pthread_exit函数, 那个线程就退出。俗称“谁调用谁退出” 

pthread_cancel函数 

在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行

主线程调用pthread_exit只是退出主线程,并不会导致进程的退出 

 线程等待

与进程类似,进程退出之后要被等待,也就是回收进程的资源,否则会出现僵尸进程,僵尸的这种状态可以通过ps指令+axj选项看到,同时会产生内存泄露的问题。


线程终止同样也需要被等待,但线程这里没有僵尸线程这样的概念,如果不等待线程同样也会造成资源泄露,也就是PCB资源未被回收,线程退出的状态我们是无法看到的,我们只能看到进程的Z状态。

pthread_join

原生线程库给我们提供了对应的等待线程的接口,其中join的第二个参数是一个输出型参数,在join的内部会拿到线程函数的返回值,然后将返回值的内容写到这个输出型参数指向的变量里面,也就是写到我们用户定义的ret指针变量里,通过这样的方式来拿到线程函数的返回值。

 线程分离 

若要进行分离,推荐创建完线程之后立马设置分离

pthread_detach

新创建出来的线程默认状态是joinable的,也就是说你必须通过pthread_join去等待线程,否则就会造成内存泄露。


但如果我们压根就不想等待线程,那调用pthread_join就是一种负担,这个时候我们就可以通过分离线程的手段,来告诉操作系统,现在我这个线程要和进程分离了,我不再共享进程的地址空间了,我也不要进程的任何资源了,我们俩人以后就形同陌路,互不相干了!操作系统你现在就把我回收吧,我已经和进程没有任何关系了!


所以在设置线程为分离状态后,操作系统会立即回收线程的所有资源,而不需要等待线程自动退出或者是手动来释放资源,表示我们现在已经不关心这个线程了!


joinable和detach是线程的两个对立的状态,一个线程不能既是joinable又是分离的,并且如果线程被设置为detach,那么就不可以用join来等待线程,否则是会报错的

int pthread_detach(pthread_t thread);

 

5.线程互斥
 

进程线程间的互斥相关背景概念

临界资源: 多线程执行流共享的资源就叫做临界资源
临界区: 每个线程内部,访问临界资源的代码,就叫做临界区
互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

 多线程共享资源访问的不安全问题(举个抢票的例子)

假设现在有一份共享资源tickets,如果我们想让多个线程都对这个资源进行操作,也就是tickets- -的操作,但下面两份代码分别出现了不同的结果,上面代码并没有出现问题,而下面代码却出现了票为负数的情况,这是怎么回事呢?

其实问题产生就是由于多线程被调度器调度的特性导致的。

抢票代码 

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<cstdio>
#include<iostream>

using namespace std;

//加锁保护
//pthread_mutex_t mtx =PTHREAD_MUTEX_INITIALIZER; 
//pthread_mutex_t  是原生线程库提供的一个数据类型


int tickets=3000;
//在并发访问的时候,会导致数据不一致的问题


void *gettickets(void *args)
{
	(void*)args;   
	         //每个线程内部,访问临界资源的代码,就叫做临界区
	
	while(true)
	{
		//pthread_mutex_lock(&mtx);  //加锁
		if(tickets>0)
		{
			usleep(10000);
			printf("%s:%d\n",(char*)args,tickets);
			tickets--;
	    // pthread_mutex_unlock(&mtx); //解锁
		}
		else
		{
	   // pthread_mutex_unlock(&mtx); 

			break;
		}
	}
	return nullptr;
}


int main()
{
	pthread_t t1,t2,t3;

    //线程创建
	pthread_create(&t1,nullptr,gettickets,(void*)"thread one");
	pthread_create(&t2,nullptr,gettickets,(void*)"thread two");
	pthread_create(&t3,nullptr,gettickets,(void*)"thread three");
	
	//线程等待
	pthread_join(t1,nullptr);
	pthread_join(t2,nullptr);
	pthread_join(t3,nullptr);


	return 0;
}

结果会出现负数(多线程在并发访问的时候,可能会导致数据不一致的问题)

要解决以上问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

谁持有锁谁才能进入临界区,你没有锁那就只能在临界区外面乖乖的阻塞等待,等待锁被释放,然后你去竞争这把锁,竞争到就拿着锁进入临界区执行代码,竞争不到就老样子,继续乖乖的在临界区外面阻塞等待

互斥量的接口
初始化互斥量有两种方法:
静态分配:
  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

 动态分配: 

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t
*restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

把上面的代码改成这样,就不会出现出现负数的问题,这里用的是静态分配 

//加锁保护
pthread_mutex_t mtx =PTHREAD_MUTEX_INITIALIZER; 

//pthread_mutex_t 是原生线程库提供的一个数据类型 ,静态


int tickets=3000;
//在并发访问的时候,会导致数据不一致的问题


void *gettickets(void *args)
{
	(void*)args;   
	         //每个线程内部,访问临界资源的代码,就叫做临界区
	
	while(true)
	{
		pthread_mutex_lock(&mtx);  //加锁
		if(tickets>0)
		{
			usleep(10000);
			printf("%s:%d\n",(char*)args,tickets);
			tickets--;
	     pthread_mutex_unlock(&mtx); //解锁
		}
		else
		{
	    pthread_mutex_unlock(&mtx); 

			break;
		}
	}
	return nullptr;
}

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

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

相关文章

使用HTML+CSS+JS网页设计与制作,酷炫动效科技农业网页

使用HTMLCSSJS网页设计与制作&#xff0c;酷炫动效科技农业网页。 可以用于家乡介绍、科技农业、图片画廊展示等个人网站的设计与制作。农业网站、家乡网站、农产品网站、旅游网站。 网站亮点 1、视觉设计&#xff1a;排版布局极简设计&#xff0c;优质的视觉体验等。 2、动…

英特尔工作站:助力专业用户实现高效创作

原创 | 文 BFT机器人 英特尔工作站是由全球知名的英特尔公司设计和开发的一款计算平台。英特尔在工作站处理器领域将其产品分为性能型和移动型两类&#xff0c;它的诞生旨在满足专业用户在科学、工程、设计等领域对高性能计算的需求。英特尔工作站配备了最新的英特尔处理器、大…

【Linux】23、内存超详细介绍

文章目录 零、资料一、内存映射1.1 TLB1.2 多级页表1.3 大页 二、虚拟内存空间分布2.1 用户空间的段2.2 内存分配和回收2.2.1 小对象2.2.2 释放 三、查看内存使用情况3.1 Buffer 和 Cache3.1.1 proc 文件系统3.1.2 案例3.1.2.1 场景 1&#xff1a;磁盘和文件写案例3.1.2.2 场景…

中通快递查询入口,根据物流更新量筛选出需要的单号记录

批量中通快递单号的物流信息&#xff0c;根据物流更新量将需要的单号记录筛选出来。 所需工具&#xff1a; 一个【快递批量查询高手】软件 中通快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;并登录 步骤2&#xff1a;点击主…

UI彩虹外链网盘系统整站源码/PHP网盘与外链分享程序/整站+模版文件

源码简介&#xff1a; 全新UI彩虹外链网盘系统源码&#xff0c;它是PHP网盘与外链分享程序&#xff0c;提供了整站模版文件&#xff0c;前后端美化模板。 彩虹外链网盘美化模板是一款专为PHP网盘和外链分享程序设计的模板。它具备多种功能&#xff0c;包括支持所有格式文件的…

单片机学习3——数码管

数码管&#xff0c;根据内部结构&#xff0c;可分为共阴极数码管和共阳极数码管。七段发光管加上一个小数点&#xff0c;共计8段。因此&#xff0c;我们对它编程的时候&#xff0c;刚好是用一个字节。 数码管的显示方式&#xff1a; 1&#xff09;静态显示&#xff1b; 2&…

小型内衣洗衣机什么牌子好?口碑最好的小型洗衣机

很多人会觉得内衣洗衣机是智商税&#xff0c;洗个内衣只需要两分钟的事情&#xff0c;需要花个几百块钱去入手一个洗衣机吗&#xff1f;然而清洗贴身衣物的并不是一件简单的事情&#xff0c;如果只是简单的搓洗&#xff0c;内裤上看不见的细菌也无法消除&#xff0c;而且对来生…

BEV+Transformer架构加速“上车”,智能驾驶市场变革开启

BEVTransformer成为了高阶智能驾驶领域最为火热的技术趋势。 近日&#xff0c;在2023年广州车展期间&#xff0c;不少车企及智能驾驶厂商都发布了BEVTransformer方案。其中&#xff0c;极越01已经实现了“BEVTransformer”的“纯视觉”方案的量产&#xff0c;成为国内唯一量产…

Vue组件的几种通信方式

这里写目录标题 Vue组件的几种通信&#xff08;数据传递&#xff09;方式非父子组件间通信&#xff08;Bus事件总线&#xff09;介绍实例 非父子通信-provide&inject1.作用2.场景3.语法4.注意 父子组件间的通信固定props属性名&#xff08;v-model&#xff09;介绍实例 不固…

PC8231(CC/CV)5V/2.4A同步降压芯片 频率可调 限流欠压补偿

一&#xff0e;概述 PC8231 是一款同步降压转换器&#xff0c; 该转换器可驱动输出 2.4A 负载电流。 设计允许 PC8231 在 9V 到40V 宽输入电压范围内工作。通过将 COMP/EN 引脚逻辑电平拉低来实现外部关断功能&#xff0c;并进入待机模式。外部补偿使反馈控制环路具有良好的线…

工业自动化配电柜监控技术,不会用就太可惜了!

随着社会的发展&#xff0c;电力系统在现代生活和工业中扮演着至关重要的角色。而配电柜作为电力系统的重要组成部分&#xff0c;其稳定运行对于保障电力供应的可靠性至关重要。 因此&#xff0c;为了提高配电柜的运行效率、确保电力系统的安全稳定运行&#xff0c;配电柜监控系…

Pycharm Available Packages显示Noting to show

使用Pycharm安装依赖包时Available packages 页面点击添加按钮后,没有任何包显示,并且无法搜索安装. 在各种网站查看到的方法如下: 1.网络问题,需要添加镜像源 点击Manage Repositories 添加一个可用的镜像源地址即可 2.打开了anaconda(那个绿色圈圈小图标),再点一下把它点…

ChatGPT进阶:提示工程的神秘面纱与实战指南

文章目录 一、提示工程的概念与原理二、提示工程的实践方法三、提示工程的挑战与展望四、实战案例分析总结《ChatGPT进阶&#xff1a;提示工程入门》内容简介作者简介陈颢鹏&#xff1a;李子菡&#xff1a; 目录获取方式 在人工智能领域&#xff0c;对话系统已经成为了一个热门…

vatee万腾的科技征途:Vatee数字化力量的新视野

在科技的浪潮中&#xff0c;Vatee万腾正展开一场引人注目的科技征途&#xff0c;以其独特的数字化力量描绘出一片新的视野。这不仅是一次技术的升级&#xff0c;更是一场对未来的全新探索&#xff0c;为我们带来了前所未有的数字化时代。 Vatee万腾以其卓越的技术实力和前瞻性的…

文件元数据批量修改:mp3音频和mp4视频的元数据如何批量修改

在数字媒体处理和管理的日常工作中&#xff0c;文件元数据的批量修改是一个常见的需求。元数据&#xff0c;或者称为文件信息&#xff0c;可以包括文件的创建日期、修改日期、文件名、文件大小、标签等。在音乐和视频处理领域&#xff0c;例如对mp3音频和mp4视频文件&#xff0…

2024年第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛正式卷任务书

2024年第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛正式卷任务书 2024年第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛正式卷A模块基础设施设置/安全加固&#xff08;200分&#xff09;A-1&#xff1a;登录安全加固&#xff08;Windows, Linux&am…

数据链路层——以太网协议、ARP协议

目录 以太网协议 以太网协议的简介 以太网协议所处的位置 以太网帧&#xff08;或者说MAC帧&#xff09;的格式 局域网通信原理 碰撞避免算法&#xff08;包含MTU的知识点&#xff09; 局域网攻击原理 ARP协议 ARP协议所在的位置 为什么要存在ARP协议&#xff08;或者…

当「华为还是备选,迪爹还是迪子」时宇宙厂一面原题

写在前面 2021 年还是互联网元年&#xff0c;当时常规的华为 Offer 还是普遍人的备选&#xff0c;如今的迪爹&#xff08;BYD&#xff09;也还是 "来投就给 Offer" 的迪子。 只有字节&#xff0c;当时是公认炙手可热的"宇宙厂"。 作为在 2021 就提前体验了…

基于helm的方式在k8s集群中部署gitlab - 备份恢复(二)

接上一篇 基于helm的方式在k8s集群中部署gitlab - 部署&#xff08;一&#xff09;&#xff0c;本篇重点介绍在k8s集群中备份gitlab的数据&#xff0c;并在虚拟机上部署相同版本的gitlab&#xff0c;然后将备份的数据进行还原恢复 文章目录 1. 备份2. 恢复到虚拟机上的gitlab2.…

加载minio中存储的静态文件html,不显示样式与js

问题描述:点击链接获取的就是纯静态文件,但是通过浏览器可以看到明明加载了css文件与js文件 原因:仔细看你会发现加载css文件显示的contentType:text/html文件,原来是minio上传文件时将所有文件的contentType设置成了text/html 要在上传时指定文件,根据文章的类型指定的Conten…