Linux中进程和线程-必知必会

大家好,我是木荣君,这次我们来聊一聊Linux中进程和线程。进程和线程的概念非常重要,本篇来详细介绍下这两者的概念。我们在实际开发中,经常会听到这两个词,如果我们不了解这些词背后的概念,那么不能称之为一个合格的软件工程师。

程序

简单来说,程序可以描述为存在磁盘文件中的任何可执行文件。它包含一组完成特定的操作指令集合。它是一个被动的实体,不会因操作系统重新启动而消失。计算机中的多任务操作就是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务。Linux就是一个支持多任务的操作系统,比起单任务系统它的功能增强了许多。多任务操作系统使用某种调度策略支持多个任务并发执行。事实上,单核处理器在某一时刻只能执行一个任务。每个任务创建时被分配时间片(ms级),任务执行(占用CPU)时,时间片递减。操作系统会在当前任务的时间片用完时调度执行其他任务。由于任务会频繁地切换执行,因此给用户多个任务同时运行的感觉。

程序、进程和线程之间的关系如下图所示

进程

进程:程序的执行实例,是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,它是操作系统进行资源分配的基本单元。进程是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

进程具有的特征:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生和消亡的;
  • 并发性:任何进程都可以同其他进程一起并发执行;
  • 独立性:进程是系统进行资源分配的基本单位;
  • 结构性:进程由指令集、数据和进程控制块三部分组成。

进程状态

Linux系统下我们可以通过ps命令查看进程的状态信息

Linux下进程相关API

特殊进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程不会浪费资源。僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

进程间通信

  • 信号 进程里可以捕捉到信号进行相应处理。
  • 管道PIPE 管道的操作也是类似文件的操作。popen()函数类似于fopen()函数,返回的是对象指针。pipe()函数类似于open()函数,返回的是对象描述符。管道是在亲属进程(同一父进程创建出的相关进程)之间进行数据传输的。
  • 命名管道FIFO 命名管道可用于无亲属关系的进程间通信。mkfifo()/mknod()将在文件系统中创建一个有路径和名称的文件。把这个管道文件当作普通文件用就行了,就可以实现进程间通信。
  • 信号量 信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。
  • 消息队列 消息队列独立于进程而存在。
  • 共享内存 通信的进程间共享一块内存进行数据交换

进程的内存模型

  • 在32位的操作系统中每一个进程的虚拟地址空间为4GB。其中1G为内核空间3G为用户空间,每一个进程的用户空间又可以分为:用户栈、共享库、用户堆、数据段和代码段。
  • 用户栈:用来保存各种临时数据,包括函数中的参数传递,和函数内的局部变量。栈的扩展方向是自顶向下,栈底地址为高地址,栈顶为低地址,栈的出入遵循先进后出原则。
  • 共享库:用来保存程序执行时所需要依赖的共享代码库,这些代码库文件的实际地址会被映射到用户栈下方的虚拟地址,并被标记为只读。
  • 用户堆:它管理的是用户程序在运行过程中动态分配的内存,需要时可以通过手动申请,用完之后手动清空。堆的扩展方向与栈相反,堆底在低地址,堆顶在高地址,当用户申请内存,堆顶指针会向上生长。
  • 数据段:主要保存的是程序中的全局变量、静态变量以及字符串常量,这些变量的生存周期通常是伴随程序的整个运行周期。
  • 代码段:代码段主要保存的是编译完成的二进制代码。

内核地址空间在进程用户态运行时通常是不可见的,只有进程进入到内核态时才能进行内核内存访问。

线程

线程-也是操作系统提供的抽象概念,是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈和线程本地存储

线程相比进程的优势

  • 线程会共享内存地址空间。
  • 创建线程花费的时间要少于创建进程花费的时间。
  • 终止线程花费的时间要少于终止进程花费的时间。
  • 线程之间上下文切换的开销, 要小于进程之间的上下文切换。
  • 线程之间数据的共享比进程之间的共享要简单。
  • 充分利用多处理器的可并行数量。线程会提高运行效率,但当线程多到一定程度后,可能会导致效率下降,因为会有线程调度切换。

线程的缺点

  • 健壮性降低:多个线程之中, 只要有一个线程不够健壮存在bug就会导致整个进程挂掉线程
  • 模型作为:一种并发的编程模型,效率并没有想象的那么高。多线程复杂度高、易出错,而且多线程难以测试和定位的问题。

通过我们巧妙的编程可以避免线程带来的缺点,合理利用多线程可以达到事半功倍的效果。而工作中多线程是不可避免的方式。

线程内存空间布局

线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

参数 1、thread:线程标识符,是一个出参 2、attr:线程属性 3、star_routine:函数指针,保存线程入口函数的地址 4、arg:给线程入口函数传参返回值:成功返回0,失败返回

创建线程时传入参数注意

  • 不可以传入临时变量。临时变量是有生命周期的,值也会改变,可能导致内存问题。
  • 结构体变量。原因和临时变量一样。
  • 可以使用堆上申请的变量。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void *ThrFun(void *arg)
{
    int *p = (int*)arg;

    printf("this is thread function:%p, data:%d\n",pthread_self(),*p);

    pthread_exit(NULL);
}
int main()
{
    int i = 1;
    pthread_t thrId;

    // 不要出入临时变量i
    int ret = pthread_create(&thrId, NULL, ThrFun, (void*)&i);

    if(ret != 0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        sleep(1);
    }

    return 0;
}

线程退出

#include <pthread.h>
void pthread_exit(void * rval_ptr)
参数说明:
rval_ptr:线程返回值,由调用pthread_join()的线程获取。
注:调用在线程内调用此函数,相当于直接调用了return,但不同的是在线程中的任意函数调用了pthread_exit()都具有退出线程的同样效果。

获取线程ID

pthread_t pthread_self(void);
返回值:返回当前线程的ID

判断两个线程ID是否相同

int pthread_equal(pthread_t t1,pthread_t t2);
参数说明:
t1:线程1的ID
t2:线程2的ID
返回值:0表示不相等,不为0表示相等
注:pthread_t是unsigned long类型 ,但不能通过值是否相等来简单的判断t1、t2是否相等,需要通过此函数判断。

终止线程

int pthread_join( pthread_t thread_id, void **retvalue ); 
参数说明:
thread_id:要连接的线程的ID
retvalue:该值非空时,将保存线程终止前调用return或pthread_exit()时的返回值。
注: 若线程未分离,则必须要调用pthread_join()进行连接,否则在线程终止时将产生僵尸线程。僵尸线程过多将无法创建新的线程
  若传入之间已经连接过的线程ID,将导致无法预知行为。

分离线程

#include<pthread.h>
int pthread_detach(pthread_t thread_id);
参数说明:
thread_id:要分离的线程ID
返回值:0表示成功,或返回大于零的错误码
注:一旦分离,无法恢复可连接状态,它是只影响终止线程以后的事情。
   可以这样使用pthread_detach(pthread_self());

多线程同步

线程同步的两种方式:互斥量(mutex)和条件变量(condition)

互斥量

防止多个线程同时访问同一共享变量

# 创建

静态初始化
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;

动态初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 
参数说明:
mutex:要初始化的互斥量
mutexattr:互斥量的属性,若为NULL则使用缺省(默认)的属性。
返回值:0表示成功,或返回大于0的错误码
注:
有自动或动态分配的互斥量要使用pthread_mutex_destroy()销毁,静态分配则不需要。
以下情况必须使用动态初始化
- 动态分配在堆中的互斥量
- 互斥量是在栈中分配的自动变量
- 静态分配的互斥量但不使用缺省属性

# 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
mutex:要销毁的互斥量
返回值:0表示成功,或返回大于0的错误码
注:
当互斥量未锁定,且后续无任何线程企图再锁定它时,销毁才安全
若互斥量驻留在动态分配的内存,先销毁再free此内存区域
自动分配的互斥量,在宿主函数返回前销毁
销毁的互斥量可以使用pthread_mutex_init()函数重新初始化,再次使用

# 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
mutex:需要加锁的互斥量
返回值:0表示成功,或返回其他大于0的错误码
注:若互斥量未锁定,将锁定并立即返回。若其他线程锁定,调用将阻塞线程直到互斥量被解锁,然后锁定互斥量再返回。 

int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数pthread_mutex_lock()函数一样,但不同的是若互斥量锁定会失败返回EBUSY错误。当轮询调用此函数,若存在其他很多线程使用pthread_mutex_lock等待同一互斥量,可能将永远不能获得上锁权。
尝试加锁,若互斥量已被加锁等待指定时间再返回

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
参数:
mutex:互斥量
abstime:超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间X之前可以阻塞等待,而不是说愿意阻塞Y秒)。这个超时时间是用timespec结构来表示,它用秒和纳秒来描述时间。
成功返回0,失败返回大于0错误码。

# 解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明,同加锁
注:不应该解锁未锁定的互斥量
   不应该解锁其他线程锁定的互斥量
   若不止一个线程等待某互斥量解锁,不确定哪个线程上锁该互斥量。

条件变量

条件变量允许一个线程改变某个共享变量时通知其他线程,并让其他线程一直阻塞等待(休眠等待)这一变化状态。条件变量总是与互斥量结合使用。

静态分配条件变量:
pthread_cond_t=PTHREAD_COND_INITIALIZER;

动态分配条件变量:
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *attr);
参数说明:
cond:要初始化的条件变量
attr:初始化的条件变量属性,若为NULL,则使用缺省属性。
返回值:0表示成功,或返回大于0的错误码
注:对已初始化的条件变量,再次初始化,结果行为未定义。


销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond)
参数说明:与pthread_cond_init()函数对应参数类似。
注:
自动或动态初始化的条件变量应有pthread_cond_destroy()销毁,静态分配的条件变量不用销毁。
应在没有任何线程等待条件变量销毁,动态分配的条件变量应在free前销毁,自动分配的应在宿主函数返回前销毁。
被pthread_cond_destroy()销毁的条件变量,可以再次调用pthread_cond_init()初始化后使用。

# 等待某个条件变量的函数
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
参数说明:
cond:指定等待的条件变量
mutex:配合使用的互斥锁,防止多线程pthread_cond_wait()竞争。
返回值:0表示成功,或大于0的错误码
注:
pthread_cond_wait()必须配合pthread_mutex_lock()、pthread_mutex_unlock()使用。此函数会阻塞等待直到pthread_cond_signal()或pthread_cond_broadcast()通知。
mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

# 带超时条件等待某个条件变量函数
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec   *abstime);
cond:指定等待的条件变量
mutex:配合使用的互斥锁,防止多线程pthread_cond_wait()竞争。
abstime:与pthread_mutex_timelock()对应参数相同
返回值:0表示成功,或大于0的错误码
注:此函数与pthread_cond_wait函数行为类似,超时则返回ETIMEOUT错误码。

#唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
cond:指定唤醒的条件变量
返回值:0表示成功,或大于0的错误码
注:
存在多个等待线程时按入队顺序激活其中一个会存在虚假唤醒、消息遗漏问题。虚假唤醒即多核处理器可能唤醒多个等待同一条件变量的线程,所以需要加入判断条件,例如
    pthread_mutex_lock(&lock);
    while (condition_is_false)
    { 
        pthread_cond_wait(&cond, &lock);
    }
    pthread_mutex_unlock(&lock);
消息遗漏即如果在一个线程调用pthread_cond_wait的过程中但未进入block状态,此时有线程调用了pthread_cond_signal或者pthread_cond_broadcast,那么此次消息将被遗漏掉,因为没有任何线程在pthread_cond_wait的block状态。这类问题的解决办法是设置一个pthread_cond_signal或者pthread_cond_broadcast的计数器count,在调用pthread_cond_wait之前先对这个count进行判断,如果count != 0 则说明已经错过了消息,可以不用等待,直接往下执行即可。例如:
    if (!count)
    {
        pthread_mutex_lock(&lock);
        while (condition_is_false)
        {
            pthread_cond_wait(&cond, &lock);
        }
        pthread_mutex_unlock(&lock);
    }

唤醒所有等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
参数说明:
与pthread_cond_wait()函数相同。
注:此函数数推荐使用在所有等待线程执行的任务不同,否则线程处理结果的效率可能不如pthread_cond_signal()

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 3
收藏 4
关注 19
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧