资讯动态

Linux的线程

发布时间:2026/10/15 1:51:55 来源:尧图企业网站定制
目录Linux线程概念进程线程页表线程的优点线程的缺点线程异常Linux进程VS线程Linux线程控制pthread_create与pthread_selfpthread_exitpthread_joinpthread_cancelpthread_datachLinux线程互斥进程线程间的互斥相关背景概念互斥量mutex互斥量的接口PTHREAD_MUTEX_INITIALIZER 与 pthread_mutex_initpthread_mutex_destroypthread_mutex_lock 与 pthread_mutex_unlock互斥量实现原理可重入VS线程安全概念常见的线程不安全的情况常见的线程安全的情况常见不可重入的情况常见可重入的情况可重入与线程安全联系可重入与线程安全区别常见锁概念死锁四个必要条件避免死锁线程同步条件变量条件变量接口PTHREAD_COND_INITIALIZER与pthread_cond_initpthread_cond_destroypthread_cond_signal 与 pthread_cond_broadcastpthread_cond_waitPOSIX信号量信号量接口sem_initsem_destroysem_waitsem_postLinux线程概念什么是线程在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程线程在进程内部运行本质是在进程地址空间内运行在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流进程内核观点线程是CPU调度的基本单位进程是承担分配系统资源的基本实体。每个进程都有自己独立的进程地址空间和独立的页表也就意味着所有进程在运行时本身就具有独立性。进程内部的若干个执行流线程 进程地址空间 页表 内核的数据结构 当前进程的代码和数据。线程创建线程时只创建task_struct并要求创建出来的线程是共享进程地址空间虚拟地址空间和页表。在Linux系统中进程和线程是没有进行区分的线程的切换和进程一样。进程的切换是CPU在执行OS(操作系统)的代码由操作系统进行切换的OS会比较进程地址空间是否相同如果相同的话就不需要更改进程地址空间和页表。在CPU内部有不同的硬件其中有一个硬件是高速缓存(L1,L2,L3)CPU的加载策略是局部性原理的会加载附近的资源在同进程的线程间切换高速缓存中存放的数据不用换。当进程间切换时进程地址空间和页表要更换高速缓存中的原始数据要设置为无效然后在加载当前进程的代码和数据到高速缓存中所有线程的调度成本比较低。每一个线程的PCB都存放着同进程中线程上下文信息上线文信息中有指向的要执行的代码CPU执行不同的PCB就实现了线程的并行执行了。windows和linux进程和线程的区别进程拥有进程描述符,描述地址空间,打开的文件等共享的资源,还有指向线程的TCB(线程控制块)的指针,而线程没有进程描述符,只是拥有一些少量的私有数据线程还有独立的调度算法。这是Windows 操作系统对线程的实现。Linux内核的设计者觉着线程要拥有进程一样的属性集合所以复用了进程的PCB结构体用PCB模拟线程的TCB。Linux没有真正意义上的线程而是用进程的方案模拟的线程。复用的代码机构简单好维护效率高也更安全所有Linux可以不间断的运行。实际上一款操作系统使用最频繁功能除了操作系统本身其次就是进程了。在使用电脑时主要的都是在和进程打交道进程这个设计方案如果你设计的很臃肿不好维护的话就会容易问题因为它数据结构更复杂所以效率高。因此Linux比Windows更适合做服务器。linux下的线程称为轻量级进程使用ps -aL命令可以显示当前的轻量级进程。默认情况下不带-L看到的就是一个个的进程。带-L就可以查看到每个进程内的多个轻量级进程。如果要调度的进程的PID与当前正在执行的进程PID不同就知道要切换上下文和缓存进程地址和页表通过加这个判断就可以确定是进程切换还是线程切换。页表虚拟地址空间中的地址通常以字节为单位。一个地址通常指向一个字节的数据。对于 32 位系统虚拟地址空间通常是 4 GB2^32 字节。32操作系统采用的二级页表的方式64操作系统采用的三级页表的方式。操作系统通常只为已分配或已申请的内存创建页表条目。当进程请求内存时操作系统会为其分配物理内存并创建相应的页表条目。对于虚拟地址空间中未分配的内存区域操作系统不会创建页表条目。当调用malloc函数时它首先会在虚拟地址空间中为请求的内存分配一个地址范围。这个地址范围在虚拟地址空间中是连续的但它尚未映射到物理内存。当程序尝试访问这个虚拟地址空间中的地址时如果对应的物理内存尚未分配会发生缺页中断。缺页中断发生后操作系统会处理这个中断为这个虚拟地址分配物理内存。操作系统会从可用的物理内存中分配一块足够大的连续空间用于映射到这个虚拟地址空间中的地址。一旦物理内存被分配操作系统会更新页表条目将虚拟地址空间中的页面映射到物理内存中的对应位置。页表更新确保了程序可以正确地访问虚拟地址空间中的数据。然后继续执行后续的代码。页表不仅记录了虚拟地址到物理地址的映射还包含了页面的访问权限信息。这些权限决定了进程可以对页面执行的操作类型。MMU是一种硬件设备也称为内存管理单元它位于计算机系统的中央处理器 (CPU) 和内存之间。 MMU负责处理程序发出的内存访问请求并将逻辑地址转换为物理地址实现对内存的管理和保护。char*shello world!;*ss;// 字符常最区是不允许被修改只允许被读取的。s里面保存的是指向字符串的虚拟起始地址 *s寻址的时候必定会伴随虚拟到物理的转化就是通过MMU 查页表的方式 对你的操作进行权限审查发现你的权限是只读但是的操作写所有是非法的MMU发生异常OS识别异常 异常转换成信号发送给目标进程 在从内核转换成为用户态的时候进行信号处理终止进程。线程的优点创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。概念说明计算密集型执行流的大部分任务主要以计算为主。比如加密解密、大数据查找等。IO密集型执行流的大部分任务主要以IO为主。比如刷磁盘、访问数据库、访问网络等。线程的缺点性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。健壮性降低:多线程程序中任何一个线程崩溃最后都会导致进程崩溃的现象。缺乏访问控制多个线程同时访问和修改同一个共享资源如果没有适当的同步措施可能会导致数据不一致或丢失。编程难度提高编写与调试一个多线程程序比单线程程序困难得多。线程异常单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出Linux进程VS线程进程是承担分配系统资源的基本实体。线程是进程调度的基本单位。线程拥有的独立的资源线程IDLWP一组寄存器。存储每个线程的上下文信息栈。每个线程都有临时的数据需要压栈出栈errno。C语言提供的全局变量每个线程都有自己的信号屏蔽字。调度优先级。进程的多个线程共享 同一地址空间,因此代码段Text Segment、数据段Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境文件描述符表。进程打开一个文件后其他线程也能够看到每种信号的处理方式。SIG_IGN、SIG_DFL或者自定义的信号处理函数当前工作目录。cwd用户ID和组IDLinux线程控制Linux下没有真正意义的线程而是用进程模拟的线程 (LWP) - 所以Linux不会提供直接创建线程的系统调用他会给我们最多提供创建轻量级进程的接口。要使用这些函数库要通过引入头文件pthreaad.h。链接这些线程函数库时要使用编译器命令的-lpthread选项。pthread_create与pthread_selfpthread_exit如果需要只终止某个线程而不终止整个进程,可以有三种方法:从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。pthread_join调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的总结如下:如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。进程才会考虑异常问题线程不考虑。如果线程异常了就是进程异常的问题。pthread_cancel一个线程跑起来了才能取消。虽然线程可以自己取消自己但一般不这样做我们往往是用于一个线程取消另一个线程比如主线程取消新线程。pthread_datach默认情况下线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值pthread_join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。一个线程如果被分离就无法被pthread_join, 如果pthread_join函数会报错。pthread_t到底是什么类型呢取决于实现。对于Linux目前实现的NPTL实现而言pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址。所有的线程都有自己独立的栈结构主线程用的是系统栈新线程用的是库提供的栈。struct pthread当中包含了对应线程的各种属性它通常包含线程的各种属性和上下文信息。#includestdio.h#includepthread.h#includeunistd.hvoid*Routine(void*arg){while(1){printf(次线程: %p\n,pthread_self());sleep(1);}}intmain(){pthread_t tid;pthread_create(tid,NULL,Routine,NULL);while(1){printf(主线程: %p\n,pthread_self());sleep(2);}return0;}Linux线程互斥互斥是指在同一时刻只允许一个线程或进程访问某个资源或执行某段代码的技术。锁是一种实现互斥的机制它允许线程获取对共享资源的独占访问权。互斥量是一种用于同步线程的机制它可以防止多个线程同时访问共享资源。进程线程间的互斥相关背景概念临界资源多线程执行流共享的资源就叫做临界资源临界区每个线程内部访问临界资源的代码就叫做临界区互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用原子性不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成互斥量mutex大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个线程其他线程无法获得这种变量。但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。多个线程并发的操作共享变量会带来一些问题。凡是访问同一个临界资源的线程都要进行加锁保护而且必须加同一把锁这个是一个游戏规则不能有例外。每一个线程访问临界区之前得加锁加锁本质是给临界区加锁加锁的粒度尽量要细一些。线程访问临界区的时候需要先加锁所有线程都必须要先看到同一把锁锁本身就是公共资源 加锁和解锁本身就是原子的。临界区可以是一行代码可以是一批代码。线程能被切换因为在我不在期间任何人都没有办法进入临界区因为他无法成功的申请到锁因为锁被我拿走了。这也正是体现互斥带来的串行化的表现站在其他线程的角度对其他线程有意义的状态就是锁被我申请(持有锁)锁被我释放了(不持有锁) 原子性就体现在这里。解锁的过程也被设计成为原子的。互斥量的接口PTHREAD_MUTEX_INITIALIZER 与 pthread_mutex_initpthread_mutex_destroy销毁互斥量需要注意使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量已经销毁的互斥量要确保后面不会有线程再尝试加锁pthread_mutex_lock 与 pthread_mutex_unlock加锁如果申请成功继续向下运行如果申请失败阻塞挂起。互斥量实现原理线程1执行加锁代码将数字0放到寄存器 %al线程1将mutex中的数据与寄存器%al的数据进行交换假设线程1的时间片到了线程1保存寄存器状态、程序计数器以及其他必要的上下文信息然后线程1会回到就绪队列中等待下一次调度线程2开始执行。操作系统会保存当线程2的寄存器状态、程序计数器以及其他必要的上下文信息操作系统会从就绪队列中选择一个就绪状态的进程(假设这个进程是线程1)操作系统将线程1的上下文信息包括寄存器状态和程序计数器加载到CPU中继续执行线程1的代码因为线程1的寄存器的%al 0所以线程1是不会阻塞挂起的。只有线程1进行解锁在不考虑其他线程的情况下线程2一旦被唤醒线程会再次尝试执行pthread_mutex_lock函数来获取锁。可重入VS线程安全概念线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。常见的线程不安全的情况不保护共享变量的函数函数状态随着被调用状态发生变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数常见的线程安全的情况每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性常见不可重入的情况调用了malloc/free函数因为malloc函数是用全局链表来管理堆的调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构可重入函数体内使用了静态的数据结构常见可重入的情况不使用全局变量或静态变量不使用用malloc或者new开辟出的空间不调用不可重入函数不返回静态或全局数据所有数据都有函数的调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据可重入与线程安全联系函数是可重入的那就是线程安全的函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。可重入与线程安全区别可重入函数是线程安全函数的一种线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。常见锁概念如果某个进程(或线程)在CPU运行中需要等待某个资源就绪就会在该资源的等待队列中进行排序。因为该进程(或线程)不在运行队列中而是在等待某种资源就绪的过程称为阻塞挂起。当该进程(或线程)等待的资源就绪了会将该进程(或线程)tast_struct重新挂载到运行队列中等待被调度器调度执行。当一个线程尝试获取一个互斥锁但未能成功时它会被阻塞挂起直到锁变得可用。一旦锁被释放内核会从等待队列中唤醒队列中的第一 个线程来获取锁。唤醒线程的过程就是将该线程线程的tast_struct重新挂载到运行队列中等待被调度器调度执行。一旦线程被调度并获得了CPU时间片它将重新执行pthread_mutex_lock()函数来尝试获取锁。死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。死锁四个必要条件互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系避免死锁破坏死锁的四个必要条件加锁顺序一致避免锁未释放的场景资源一次性分配线程同步同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解饥饿问题一个线程不停的加锁与解锁造成了其他线程只能等待的。条件变量当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如一个线程访问队列时发现队列为空它只能等待只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。#includeiostream#includestring#includecstdio#includepthread.h#includeunistd.husingnamespacestd;constintnum5;pthread_cond_t condPTHREAD_COND_INITIALIZER;pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER;void*active(void*args){string namestatic_castconstchar*(args);while(true){pthread_mutex_lock(mutex);pthread_cond_wait(cond,mutex);// pthread_cond_wait调用的时候会自动释放锁coutname 活动endl;pthread_mutex_unlock(mutex);}}intmain(){pthread_t tids[num];for(inti0;inum;i){char*namenewchar[32];snprintf(name,32,thread-%d,i1);pthread_create(tidsi,nullptr,active,name);}sleep(3);while(true){coutmain thread wakeup thread...endl;//pthread_cond_signal(cond);//pthread_cond_broadcast(cond);sleep(1);}for(inti0;inum;i){pthread_join(tids[i],nullptr);}return0;}条件变量接口PTHREAD_COND_INITIALIZER与pthread_cond_initpthread_cond_destroypthread_cond_signal 与 pthread_cond_broadcastpthread_cond_wait//交叉打印100以内的数#includeiostream#includepthread.h#includestringusingnamespacestd;inti1;pthread_mutex_t mutex;pthread_cond_t cond;void*myprintf_1(void*arg){string namestatic_castconstchar*(arg);while(i100){pthread_mutex_lock(mutex);if(i%2!1){pthread_cond_wait(cond,mutex);}std::coutnameistd::endl;pthread_mutex_unlock(mutex);pthread_cond_signal(cond);}}void*myprintf_2(void*arg){string namestatic_castconstchar*(arg);while(i100){pthread_mutex_lock(mutex);if(i%2!0){pthread_cond_wait(cond,mutex);}std::coutnameistd::endl;pthread_mutex_unlock(mutex);pthread_cond_signal(cond);}}intmain(){pthread_mutex_init(mutex,nullptr);pthread_cond_init(cond,nullptr);pthread_t t1;pthread_t t2;pthread_create(t1,nullptr,myprintf_1,(void*)thread 1: );pthread_create(t2,nullptr,myprintf_2,(void*)thread 2: );pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_mutex_destroy(mutex);pthread_cond_destroy(cond);return0;}POSIX信号量信号量本质就是一个计算器信号量是对临界资源的预定机制。我们将可能会被多个执行流同时访问的资源叫做临界资源临界资源需要进行保护否则会出现数据不一致等问题。当我们仅用一个互斥锁对临界资源进行保护时相当于我们将这块临界资源看作一个整体同一时刻只允许一个执行流对这块临界资源进行访问。但实际我们可以将这块临界资源再分割为多个区域当多个执行流需要访问临界资源时如果这些执行流访问的是临界资源的不同区域那么我们可以让这些执行流同时访问临界资源的不同区域此时不会出现数据不一致等问题。信号量接口sem_initsem_destroysem_waitsem_wait信号量的值大于零则将其减一并允许调用线程继续执行如果信号量的值为零则调用线程将被阻塞直到信号量的值大于零为止。sem_wait函数是原子性的sem_postsem_post来增加信号量的值并可能唤醒一个或多个等待该信号量的线程。sem_post操作也是原子性的

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑