欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 【Linux线程】多线程控制(深度解析,小白一看就懂!!)

【Linux线程】多线程控制(深度解析,小白一看就懂!!)

2025/3/13 2:56:19 来源:https://blog.csdn.net/weixin_45031801/article/details/141286219  浏览:    关键词:【Linux线程】多线程控制(深度解析,小白一看就懂!!)

目录

一、前言

二、线程的知识细节补充 

🔥线程私有资源🔥 

🔥线程共享资源🔥  

🔥原生线程库🔥   

三、线程控制接口 

💧 线程的创建💧

💧 多线程创建💧 

💧 线程等待💧  

💧 线程终止💧   

💧 多线程编程实战💧    

💧其他接口💧 

四、深度理解线程 

🍇理解线程ID 

🥝理解线程独立栈 

🍋理解线程局部存储

五、共勉 


一、前言

线程】是进程内部的一个执行流作为 CPU 运行的基本单位,对于线程的合理控制与任务的执行效率息息相关,因此掌握线程基本操作(线程控制)是很有必要的

二、线程的知识细节补充 

在正式介绍线程控制相关接口前,需要先补充一波线程相关知识 

🔥线程私有资源🔥 

在 【线程的深度解析中】 中我们得出了一个结论:Linux 中没有真线程,只有复用 PCB 设计思想的 TCB 结构 

  •  在 Linux 中,认为 【PCB】 与 【TCB】 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 【线程管理】 时,完全可以复用 【进程管理】 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的,有的只是复用 PCB 设计思想的 TCB
  •  在这种设计思想下,【线程】 注定不会过于庞大,因此 Linux 中的 线程 又可以称为 轻量级进程(LWP),轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得 Linux 系统不间断的运行程序,不会轻易 崩溃

因此 Linux 中的线程本质上就是 轻量级进程(LWP,一个进程内的【多个线程】看到的是同一个进程地址空间,所以所有的线程可能会共享进程的大部分资源 


但是如果多个执行流(多个线程)都使用同一份资源,如何确保自己的相对独立性呢? 

  • 相对独立性:线程各司其职,不至于乱成一锅粥

  • 显然,多线程虽然共同 “生活” 在一个进程中,但也需要有自己的 “隐私”,而这正是 线程私有资源 

 线程私有资源:

  • 线程 ID:内核观点中的 LWP
  • 一组寄存器: 线程切换时,当前线程的上下文数据需要被保存
  • 线程独立栈: 线程在执行函数时,需要创建临时变量
  • 错误码 errno: 线程因错误终止时,需要告知父进程
  • 信号屏蔽字: 不同线程对于信号的屏蔽需求不同
  • 调度优先级: 线程也是要被调度的,需要根据优先级进行合理调度

其中,线程 最重要 的资源是 一组寄存器(体现切换特性)和独立栈(体现临时运行特性) 


🔥线程共享资源🔥  

 除了上述提到的 线程私有资源 外,【多线程】 还共享着进程中的部分资源

  • 共享的定义不需要太多的额外成本,就可以实现随时访问资源 
  • 基于 多线程看到的是同一块进程地址空间,理论上 凡是在进程地址空间中出现的资源,多线程都是可以看到的
  • 但实际上为了确保线程调度、运行时的独立性,只能共享部分资源

这也就是线程中的栈区称作 “独立栈” 的原因:某块栈空间属于某个线程,其他线程是可以访问的,为了确保独立性,并不会这样做 

 在 Linux 中,多线程共享资源如下

  • 共享区、全局数据区、字符常量区、代码区: 常规资源共享区
  • 文件描述符表: 进行 IO 操作时,无需再次打开文件
  • 每种信号的处理方式: 多线程共同构成一个整体,信号的处理动作必须统一
  • 当前工作目录: 即使是多线程,也是位于同一工作目录下
  • 用户 ID 和 组 ID: 进程属于某个组中的某个用户,多线程也是如此

🔥原生线程库🔥   

在之前编译多线程相关代码时,我们必须带上一个选项:-lpthread,否则就无法使用多线程相关接口

  •  带上这个选项的目的很简单:使用 pthread 原生线程库

接下来对 原生线程库 进行一个系统性的理解 

  • 首先,在 Linux 中是没有真正意义上的线程,有的只是通过进程模拟实现的线程(LWP 
  • 站在操作系统角度:并不会提供对线程控制的相关接口,最多提供轻量级进程操作的相关接口 
  • 但是对于用户来说,只认识线程,并不清楚轻量级进程 

所以为了使用户能愉快的对线程进行操作,就需要对系统提供的轻量级进程操作相关接口进行封装:对下封装轻量级进程操作相关接口,对上给用户提供线程控制的相关接口 

在 Linux 中,封装轻量级进程操作相关接口的库称为 pthread 库,即 原生线程库,这个库文件是所有 Linux 系统都必须预载的,用户使用多线程控制相关接口时,只需要指明使用 -lpthread 库,即可正常使用多线程控制相关接口 


三、线程控制接口 

 有了前面知识的补充之后,接下来正式进入线程控制接口的学习

💧 线程的创建💧

要想控制线程,得先创建线程,对于 原生线程库 来说,创建线程使用的是 pthread_create 这个接口

#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
  • 先来认识一下函数中涉及的参数 

参数1: pthread_t* 线程 ID,用于标识线程,其实这玩意本质上就是一个 unsigned long int 类型 

  •  注:pthread_t* 表明这是一个输出型参数,旨在创建线程后,获取新线程 ID

参数2: const pthread_attr_t* 用于设置线程的属性,比如优先级、状态、私有栈大小

  •  这个参数一般不考虑,直接传递 nullptr 使用默认设置即可 

参数3: void *(*start_routine) (void *) 这是一个很重要的参数,它是一个 返回值为 void* 参数也为 void* 的函数指针,线程启动时,会自动回调此函数

  • 指向线程函数的指针,即新线程将执行的函数。 

参数4: void* 显然,这个类型与回调函数中的参数类型匹配上了,而这正是线程运行时,传递给回调函数的参数 

  •  返回值 int创建成功返回 0,失败返回 error number

注意事项 

  • 线程函数必须有 void* 类型的参数,并返回 void* 类型的结果。
  • 需要包含头文件 #include <pthread.h>
  • 在编译时需要链接 pthread 库,例如使用 -lpthread 选项。

明白创建线程函数的各个参数后,就可以尝试创建一个线程了 

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;// 线程的回调函数
void* threadRun(void *arg)
{while(true){cout << "我是次线程,我正在运行..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;  // 线程ID // pthread_create 创建线程   // pthread_create(&线程ID , 线程属性(通常为nullptr), 线程启动调用的函数 , 线程返回值(没设置就是nullptr))pthread_create(&t, nullptr, threadRun, nullptr);  while(true){cout << "我是主线程 " << " 我创建了一个次线程 " << t << endl;sleep(1);}return 0;
}

  • 代码运行成功 ,同时我们还可以检测两个线程 
while :; do ps -aL | head -1 && ps -aL | grep thread ; echo "-----------------------------------"; sleep 1 ; done


 .如何验证 原生线程库 存在?

现在我们已经得到了一个链接 原生线程库 的可执行程序,可以通过 ldd 可执行程序 查看库的链接情况 

ldd thread

  • 足以证明原生线程库确确实实的存在于我们的系统中 

💧 多线程创建💧 

 根据上面演示的线程创建,我们再来看看多线程的创建

#include <iostream>
#include <unistd.h>
#include <pthread.h>  // 线程库using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}return nullptr;
}int main()
{pthread_t pt[NUM];  // 将线程ID 放入数组中,为创建多个线程做准备for(int i = 0; i < NUM; i++){// 注册新线程的信息char name[64];// 将"thread-%d" 转换为字符串 放在 name中snprintf(name, sizeof(name), "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name); // 创建新线程,并给予其name名称}while(true){cout << "我是主线程,我正在运行" << endl;sleep(1);}return 0;
}
  • 细节传递 pthread_create 的参数1时,可以通过 起始地址+偏移量 的方式进行传递,传递的就是 pthread_t*
  • 预期结果:打印 thread-1thread-2thread-3 … 

  •  实际结果:确实有五个次线程在运行,但打印的结果全是 thread-5
  •  原因:char name[64] 属于主线程中栈区之上的变量,多个线程实际指向的是同一块空间,最后一次覆盖后,所有线程都打印 thread-5

这是由于多线程共享同一块区域引发的问题,解决方法就是在堆区动态匹配空间,使不同的线程读取不同的空间,这样就能确保各自信息的独立性

#include <iostream>
#include <unistd.h>
#include <pthread.h>  // 线程库using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];  // 将线程ID 放入数组中,为创建多个线程做准备for(int i = 0; i < NUM; i++){// 注册新线程的信息char* name = new char[64];// 将"thread-%d" 转换为字符串 放在 name中snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name); // 创建新线程,并给予其name名称}while(true){cout << "我是主线程,我正在运行" << endl;sleep(1);}return 0;
}

  • 显然,线程每次的运行顺序取决于调度器 

在上面的程序中,主线程也是在死循环式运行,假若主线程等待 3 秒后,再 return, 会发生什么呢?

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待 3 秒后 returnsleep(3);return 0;
}

  •  结果:程序运行 3 秒后,主线程退出,同时其他次线程也被强制结束了
  • 这是因为 主线程结束了,整个进程的资源都得被释放,次线程自然也就无法继续运行了 

换句话说,次线程由主线程创建,主线程就得对他们负责,必须等待他们运行结束,类似于父子进程间的等待机制;如果不等待,就会引发僵尸进程问题,不过线程这里没有僵尸线程的概念,直接影响就是次线程也全部退出了 


💧 线程等待💧  

 主线程需要等待次线程,在 原生线程库 中刚好存在这样一个接口 pthread_join,用于等待次线程运行结束

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
  • 参数1: pthread_t 待等待的线程 ID,本质上就是一个无符号长整型类型;这里传递是数值,并非地址
  • 参数2: void** 这是一个输出型参数,用于获取次线程的退出结果,如果不关心,可以传递 nullptr 
  • 返回值:成功返回 0,失败返回 error number 

函数原型很简单,使用也很简单,我们可以直接在主线程中调用并等待所有次线程运行结束 

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>  // 线程库using namespace std;#define NUM 5void* threadRun(void *name)
{int count = 5;while(count>0){cout << "我是次线程 " << (char*)name <<" "<< "pid :"<<getpid()<<endl;sleep(1);count--;}delete[] (char*)name;return (void*)666;
}int main()
{pthread_t tid;  // 创建线程ID// 注册新线程的信息char* name = new char[64];// 将"my name is xas" 转换为字符串 放在 name中snprintf(name, 64, "my name is xas");pthread_create(&tid, nullptr, threadRun, name); // 创建新线程,并给予其name名称cout << "我是主线程 " << "pid :"<<getpid()<<endl;void* ret = nullptr;  // 获取线程返回值int n = pthread_join(tid , &ret);cout<<"main thread quit , n = "<< n <<" "<< "main thread get a ret : "<<(long long)ret<<endl;return 0;
}


💧 线程终止💧   

线程可以被创建并运行,也可以被终止,线程终止方式有很多种 

  • 比如 等待线程回调函数执行结束,次线程运行五秒后就结束了,然后被主线程中的 pthread_join 等待成功,次线程使命完成 
void* threadRun(void *name)
{int count = 5;while(count>0){cout << "我是次线程 " << (char*)name <<" "<< "pid :"<<getpid()<<endl;sleep(1);count--;}delete[] (char*)name;return (void*)666;
}
  • 其实 原生线程库 中有专门终止线程运行的接口 pthread_exit,专门用来细粒度地终止线程,谁调用就终止谁,不会误伤其他线程
#include <pthread.h>void pthread_exit(void *retval);
  • 仅有一个参数 void*用于传递线程退出时的信息 

这个参数名叫 retvalpthread_join 中的参数2也叫 retval,两者有什么不可告人的秘密吗?

  •  答案是这俩其实本质上是同一个东西,pthread_join 中的 void **retval 是一个输出型参数,可以把一个 void * 指针的地址传递给 pthread_join 函数,当线程调用 pthread_exit 退出时,可以根据此地址对 retval 赋值,从而起到将退出信息返回给主线程的作用

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 3void* threadRun(void *name)
{cout << "我是次线程 " << (char*)name << endl;sleep(1);delete[] (char*)name;pthread_exit((void*)"EXIT");// 直接return "EXIT" 也是可以的// return (void*)"EXIT";
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次线程运行结束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0){cerr << "等待线程 " << pt[i] << " 失败!" << endl;}cout << "线程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;}cout << "所有线程都退出了" << endl;return 0;
}


💧 多线程编程实战💧    

无论是 pthread_create 还是 pthread_join,他们的参数都有一个共同点:包含了一个 void* 类型的参数,这就是意味着我们可以给线程传递对象,并借此进行某种任务处理 

比如我们先创建一个包含一下信息的线程信息类用于计算 [0, N] 的累加和 

  • 线程名字(包含 ID
  • 线程编号
  • 线程创建时间
  • 待计算的值 N
  • 计算结果
  • 状态

为了方便访问成员,权限设为 public 

// 线程信息类的状态
enum class Status
{OK = 0,ERROR
};// 线程信息类
class ThreadData
{
public:ThreadData(const string &name, int id, int n):_name(name),_id(id),_createTime(time(nullptr)),_n(n),_result(0),_status(Status::OK){}public:string _name;  // 线程名称int _id;       // 线程编号time_t _createTime;int _n;        // 累加到几int _result;   // 运算相加的结果Status _status;  //线程状态
};
  •  此时就可以编写 回调方法 中的业务逻辑了
void* threadRun(void *arg)
{ThreadData *td = static_cast<ThreadData*>(arg);// 业务处理for(int i = 0; i <= td->_n; i++)td->_result += i;// 如果业务处理过程中发现异常行为,可以设置 _status 为 ERRORcout << "线程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " done..." << endl;pthread_exit((void*)td);// 也可以直接 return // return td;
}

 主线程在创建线程及等待线程时,就可以使用 ThreadData 对象了,后续涉及业务修改时,也只需要修改类及回调方法即可,无需再更改创建及等待逻辑,有效做到了 解耦

int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);// 创建对象ThreadData *td = new ThreadData(name, i, 100 * (10 + i));pthread_create(pt + i, nullptr, threadRun, td);sleep(1); // 尽量拉开创建时间}// 等待次线程运行结束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待线程 " << pt[i] << " 失败!" << endl;ThreadData *td = static_cast<ThreadData*>(retval);if(td->_status == Status::OK)cout << "线程 " << pt[i] << " 计算 [0, " << td->_n << "] 的累加和结果为 " << td->_result << endl;delete td;}cout << "所有线程都退出了" << endl;return 0;
}
  •  程序可以正常运行,各个线程也都能正常计算出结果;这里只是简单计算累加和,线程还可以用于其他场景:网络传输、密集型计算、多路 IO等,无非就是修改线程的业务逻辑

结论:多线程可以传递对象指针,自由进行任务处理


💧其他接口💧 

 与多线程相关的还有一批其他接口,比较简单,就放在一起介绍了

关闭线程 

线程可以被创建,自然也可以被关闭,可以使用 pthread_cancel 关闭已经创建并运行中的线程 

#include <pthread.h>int pthread_cancel(pthread_t thread);
  • 参数1: pthread_t 被关闭的线程 ID
  • 返回值成功返回 0,失败返回一个非零的 error number

这里可以直接模拟关闭线程的场景 

#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{// 将线程名 类型强制转换const char *ps = static_cast<const char*>(arg);while(true){cout << "线程 " << ps << " 正在运行" << endl;sleep(1);}// 终止线程,并返回10pthread_exit((void*)10);
}int main()
{pthread_t t; // 创建线程 ID// 创建线程  (void*)"Hello Thread" -- 为线程名pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");// 3秒后关闭线程sleep(3);// 关闭线程pthread_cancel(t);void *retval = nullptr;pthread_join(t, &retval);// 细节:使用 int64_t 而非 uint64_tcout << "线程 " << t << " 已退出,退出信息为 " << (int64_t)retval << endl;return 0;
}

  •  程序运行 3 秒后,可以看到退出信息为 -1,与我们预设的 10 不相符
  • 原因很简单:只要是被 pthread_cancel 关闭的线程,退出信息统一为 PTHREAD_CANCELED 即 -1
  • 这也就解释了为什么要强转为 ingt64_t,因为无符号的 -1 非常大,不太好看

获取线程ID 

线程 ID 是线程的唯一标识符,可以通过 pthread_self 获取当前线程的 ID 

#include <pthread.h>pthread_t pthread_self(void);
  •  返回值:当前线程的 ID
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{// 强制类型转换const char* ps = static_cast<const char*>(arg);cout <<"新线程"<<ps<<" "<< "当前次线程的ID为 " << pthread_self() << endl;return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, (void*)"Hello thread");pthread_join(t, nullptr);cout << "创建的次线程ID为 " << t << endl;return 0;
}

  •  可以看到结果都是一样的

四、深度理解线程 

🍇理解线程ID 

在见识过 原生线程库 提供的一批便利接口后,不由得感叹库的强大,如此强大的库究竟是如何工作的呢?

  • 原生线程库本质上也是一个文件,是一个存储在 /lib 目录下的动态库,要想使用这个库,就得在编译时带上 -lpthread 指明使用动态库

程序运行时,原生线程库 需要从 磁盘 加载至 内存 中,再通过 进程地址空间 映射至 共享区 中供线程使用 

  • 由于用户并不会直接使用 轻量级进程 的接口,于是 需要借助第三方库进行封装,类似于用户可能不了解系统提供的 文件接口,从而使用 C语言 封装的 FILE 库一样

  • 对于 原生线程库 来说,线程不止一个,因此遵循 先描述,再组织 原则,在线程库中创建 TCB 结构(类似于 PCB),其中存储 线程 的各种信息,比如 线程独立栈 信息

  • 在内存中,整个 线程库 就像一个 “数组”,其中的一块块空间聚合排布 TCB 信息,而 每个 TCB 的起始地址就表示当前线程的 ID,地址是唯一的,因此线程 ID 也是唯一的 

因此,我们之前打印 pthread_t 类型的 线程 ID 时,实际打印的是地址,不过是以 十进制 显示的,可以通过函数将地址转化为使用 十六进制 显示 

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "我是[次线程],我的ID是 " << toHex(pthread_self()) << endl;return (void*)0;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "我是[主线程],我的ID是 " << toHex(pthread_self()) << endl;return 0;
}

线程 ID 确实能转化为地址虚拟进程地址空间上的地址 

🥝理解线程独立栈 

 线程 之间存在 独立栈,可以保证彼此之前执行任务时不会相互干扰,可以通过代码证明

  • 多个线程使用同一个入口函数,并打印其中临时变量的地址 
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%lx", t);return id;
}void *threadRun(void *arg)
{int tmp = 0;cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;return (void*)0;
}int main()
{pthread_t t[5];for(int i = 0; i < 5; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 5; i++)pthread_join(t[i], nullptr);return 0;
}
  • 可以看到五个线程打印 “同一个” 临时变量的地址并不相同,足以证明 线程独立栈 的存在 

 注意:

  1. 所有线程都要有自己独立的栈结构(独立栈),主线程中用的是进程系统栈次线程用的是库中提供的栈
  2. 多个线程调用同一个入口函数(回调方法),其中的局部变量地址一定不一样,因为存储在线程独立栈中

🍋理解线程局部存储

线程 之间共享 全局变量,对 全局变量 进行操作时,会影响其他线程 

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%lx", t);return id;
}void *threadRun(void *arg)
{cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;return (void*)0;
}int main()
{pthread_t t[3];for(int i = 0; i < 3; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 3; i++)pthread_join(t[i], nullptr);return 0;
}

  • 在三个线程的影响下,g_val 最终变成了 103 

如何让全局变量私有化呢?即每个线程看到的全局变量不同 

  • 可以给全局变量加 __thread 修饰,修饰之后,全局变量不再存储至全局数据区,而且存储至线程的 局部存储区中 
__thread int g_val = 100;

  • 结果:修饰之后,每个线程确实看到了不同的 “全局变量”
  • 特点:此时的 “全局变量” 的地址变大了

 “全局变量” 地址变大是因为此时它不再存储在 全局数据区 中,而且存储在线程的 局部存储区 中,线程的局部存储区位于 共享区,并且 共享区 的地址天然大于 全局数据区

注意: 局部存储区位于共享区中,可以通过 __thread 修饰来改变变量的存储位置 


五、共勉 

 以下就是我对【Linux系统编程】线程控制的深度解析 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新【Linux系统编程】,请持续关注我哦!!!    

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词