线程是操作系统能够进行运算调度的基本单位,它被包含在进程之中,是进程中的实际运作单位。
定义与基本特征
线程的状态
作用与优势
与进程的关系
线程的缺点
在操作系统中,线程是实现并发编程和提高系统性能的重要手段,广泛应用于各种类型的程序开发中。
这里我们举一个简单的例子帮助大家理解。我们都知道,盖一栋大楼需要土木的同学画图纸,建筑工人打地基,吊车工人运送原料,设计的同学设计室内,货车司机运水泥等等。他们共同在做的就是盖大楼,但他们每个人都在做自己的事情,这些事情是无法分开的,但合起来就是盖楼的一个过程,这就是我们进程和线程之间的关系。每个人做的工作都是线程,所有人做的工作合起来就是进程。
我们以前说的进程就是单线程进程,开个玩笑说一个人把楼给盖起来的。
所以我们要理解的是,进程和线程真正的概念。进程是承担分配系统资源的基本实体,线程是我们进程内部的执行流资源。简单来说,由进程来帮我们申请空间分配资源,线程在进程分配的资源中进行线程的执行。
线程和进程的切换问题。线程作为一个轻量级的进程,在CPU中也是要进行切换的。一个进程中的线程共享这个进程的时间片,描述线程的数据结构也是task_struct结构,当然其中有标识它是线程的标识符id。在我们CPU执行进程的时候,会将缓存数据加载到寄存器cache中,在线程进行切换的时候,由于共享数据的特性,cache中的数据不用进行更换,效率高,时间片结束后,进程切换时,才进行cache数据更换。
今天我们借线程这个话题再谈进程地址空间。线程是进程的细分,这个细分也要由页表找到物理内存,它是怎么找到的呢,这里我们就不得不讲解一下页表的实现结构了。
实际上,页表并不像我们前面说的一列是虚拟内存地址,另一列是物理内存地址,它们一一对应,这只是我们抽象出来的,忽略了页表自身结构,而保留它的功能的一个抽象的概念。有一个很简单的问题,如果页表的实现是这样的话,以32位计算机为例,它的一个地址就需要页表中4个字节来存储,而我们的虚拟地址中有页的概念,一页4KB,虚拟内存一共有多少页那页表就有多少虚拟地址,已知虚拟内存共4GB,也就是4*1024*1024÷4个地址,那光虚拟内存就要4*1024*1024bytes = 4MB连续的物理内存,并且大部分进程实际使用的虚拟地址空间只是其中一小部分,这就意味着页表中大量的页表项是无效的,却依然占用着物理内存,造成了极大的浪费,所以我们就要引出真正的页表结构了。
多级页表
我们页表的结构就是多级页表,上面所说的这种页表结构叫做一级页表,这样的页表我们也看到了,会占用大量连续的物理内存,并且很多情况下大部分页表项可能都是无效的,造成内存浪费,是不可行的。多级页表结构通过将页表分级,仅在需要时才分配和使用各级页表,从而有效减少内存占用。
二级页表
虚拟地址需要32个比特位来存储,我们将这32个比特位分开存储,前十个,中间十个,最后十二个共三个部分。第一部分叫做外层页表,第二部分叫做内层页表,第三部分叫做页内偏移。
外层页表(页目录)
前十个比特位叫做外层页表(页目录),页目录中的每个条目都指向相对应的一个内层页表。把前十位看做一个十进制数字,这个数字的大小就是页目录的下标,页目录占用空间的大小为2^10*4 bytes = 4KB,页目录的条目数和内层页表的条目数相同,是一一对应的。
内层页表(二级页表)
中间十个比特位叫做内层页表(二级页表),二级页表中的每个条目都指向物理地址空间的一个页。把中间十位看做一个十进制数字,这个数字的大小,就是该地址在物理地址空间的页码下标。一个二级页表的大小也是4KB,但是二级页表一般都是不全的,申请才有,不申请就没有。
页内偏移
最后十二个比特位叫做页内偏移(偏移量)。前面已经指向了物理地址空间的一个页了,我们知道页的大小就是4KB,也就是2^12字节,而最后剩下十二个比特位正是用来形容它的。我们知道每个字节都有一个地址,这里页内偏移就是地址相对于这个页起始地址的偏移量。
库函数pthread_create
用于创建一个新线程。
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功返回0,失败返回非零错误码。
thread:这是一个指向 pthread_t 类型变量的指针,pthread_t 是一个线程标识符类型,用于唯一标识一个线程。当 pthread_create 成功创建一个新线程时,会将该线程的标识符存储在 *thread 所指向的内存位置。
attr:这是一个指向 pthread_attr_t 类型的常量指针,用于指定新线程的属性。如果将其设置为 NULL,则表示使用默认的线程属性。pthread_attr_t 类型定义了一系列线程的属性,如线程的栈大小、调度策略、分离状态等。可以使用 pthread_attr_init 函数初始化一个 pthread_attr_t 对象,并使用其他相关函数来设置具体的属性,但是我们一般用不到这些属性,我们线程用来就是实现多任务调度的。
start_routine:这是一个函数指针,指向新线程开始执行时要调用的函数。该函数必须接受一个 void * 类型的参数,并返回一个 void * 类型的值。新线程从这个函数开始执行,直到该函数返回或者线程被取消。
arg:这是传递给 start_routine 函数的参数。由于 start_routine 函数的参数类型是 void*,因此可以将任意类型的数据指针转换为 void* 类型传递给该函数。在 start_routine 函数内部,需要将其转换回原来的类型,这个我们下一篇文章再谈,这里就简单使用一下。
测试
下面是一个简单的测试线程创建的代码。主线程和新线程的任务都是循环打印自己的pid。
#include <iostream> #include <pthread.h> #include <unistd.h> void *threadRun(void* args){ while(1) { std::cout
注意这里的makefile文件,我们将所对应的库写上了,这个库叫做POSIX线程库,它并不是操作系统原生自带,但在Linux系统中都会有这个库,因为Linux内核提供的clone()函数调用更加复杂,clone是Linux内核用来创建轻量级进程的函数,我们的pthread_create()就是基于它封装的,使用方便。
简单看一下clone函数,确实是要比pthread_create()要难用的。
#include <sched.h> int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
我们查看打印结果,虽然因为进程调度的原因偶尔会出现重叠乱打的情况,但是大部分时间还是正常的。从这个乱打的情况下我们可以发现,线程之间是不存在同步和互斥的概念的。我们还可以通过打印结果发现一个特点,它们的进程pid是相同的,即一个进程中的多个线程共享一个进程pid。
在进程执行的过程中,我们可以通过命令ps -aL查看线程资源。我们线程也有自己的唯一标识符LWP表示light weight process也就是线程。我们发现两个线程的PID相同这我们上面说了,其中一个线程LWP与PID相同,这个线程就是主线程。
今日分享就到这了~
以上就是【Linux】初识线程的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号