POSIX 详解

发布时间 2023-09-27 11:44:18作者: 黄河大道东

编写跨平台应用需要考虑的问题

  假如一个美国人、一个德国人、一个法国人在一起想开展贸易,他们互相听不懂对方的语言,但是如果不懂对方语言,就无法开展贸易,怎么办呢,于是他们坐下来决定以后在贸易市场中都使用英语来交流,这样就可以互相之间谈生意了。那么在贸易市场中都使用英语来交流这个规则就是一个大家都遵守的标准

POSIX是什么,为什么需要POSIX

  POSIX 是可移植操作系统接口 Portable Operating System Interface of UNIX
的缩写, POSIX 标准定义了操作系统应该为应用程序提供的接口标准,是在各种UNIX操作系统上运行的软件的一系列 API标准的总称 。它定义了一套标准的操作系统接口和工具,最初是基于 UNIX 制定的针对操作系统应用接口的国际标准。 POSIX 是一个涵盖范围很广的标准体系,距今已经颁布了20多个标准。

  制定POSIX标准是为了获得不同操作系统在源代码级的软件兼容性,使操作系统具有较强的可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。任何操作系统都只有符合该标准才能运行UNIX程序。

  看到这里,应该就明白了POSIX本质上就是为了让一种UNIX系统上开发的程序能在另一种UNIX系统运行而制定的一种标准,不这样的话,不同厂商的 UNIX 系统各自开发的软件都不能在别家的 UNIX 系统上运行了。而POSIX这个标准就是为了解决这个问题而生的,各厂商在开发UNIX系统时,只要遵守这个 POSIX 标准,开发出来的系统就可以运行其他遵守 POSIX 标准的软件。

  Linux就是 是一个遵循 POSIX 标准的操作系统。也就是说,任何基于 POSIX 标准编写的应用程序,包括大多数UNIX和类UNIX系统的应用程序,都可以方便地移植到Linux系统上。

  同时 POSIX 并不局限于 UNIX,许多其它的操作系统也支持POSIX,例如Windows从WinNT开始就有兼容POSIX的考虑。这是因为当年在要求严格的领域,Unix地位比Windows高。为了把Unix用户拉到Windows阵营搞的。现在情况当然有变化,与当年大不相同了。现在最新的Win10对Linux/POSIX支持好,则是因为Linux已经统治了廉价服务器市场。为了提高Windows的竞争力搞的。windows遵循这个标准的好处是软件可以跨平台。所以windows也支持就很容易理解,很多多优秀的开源软件,支持了这个这些软件就可能有windows版本,就可以完善丰富windows下的软件。

  一般情况下,应用程序通过应用编程接口(API),而不是直接通过系统调用来编程。这点很重要,因为应用程序使用API实际上并不需要和内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口。它们可以调用一个系统调用,也可以通过调用多个系统调用来实现,而不使用任何系统调用也可以。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却不同。

  从这种意义上来说,程序员不关心系统调用,他们只需要使用好API,而操作系统只需要处理好系统调用。不同的操作系统内核实现同样的功能的方法不同,为了实现可移植性,不同操作系统需要遵循同一套标准。

  举例来说,系统A实现fork的系统调用是A_fork,系统B实现fork的系统调用是B_fork,给系统A编写的程序如果要移植到系统B上,需要修改每一处调用A_fork的代码。如果系统A和B都遵循标准POSIX,把自己的fork封装到一个通用的POSIX_fork调用里,然后把这样遵循标准的函数都集中到unistd.h头文件里,那么应用程序只需要用这个头文件里的函数就可以实现不同系统之间的移植。

POSIX线程常用API介绍

1、POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_开头的
  • 要使用线程库中的函数,要通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的-lpthread选项

2、创建线程pthread_create

  • 功能:创建一个新的线程

  • 函数签名:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

  • 参数说明

    • thread:是一个输出型参数,返回线程ID,常用来保存线程id(无符号长整形)
    • attr:设置线程的属性,attr为NULL表示使用默认属性,一般都是NULL
    • start_routine:是个函数地址(函数指针),线程启动后要执行的函数
    • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码

3、pthread_self

pthread_self()函数可以返回当前线程的id,这是一个pthread线程结构体的地址

示例代码

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdlib>
#include <cstring>
using namespace std;
 
void *thread_run(void* args){
    char *s = (char*)args; // 首先要将void*的参数转成对应的类型
    while(1) {
        cout << "Get the param :" << s << endl;
        printf("new thread id: 0x%x\n", pthread_self());
        sleep(2);
    }
}

int main() {
    char *s = new char[6];
    strcpy(s,"zebra");
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, s);
    while ( true ) {
        printf("main thread id: 0x%x\n", pthread_self());
        sleep(2);
    }
}

输出
image

  可以看到,主线程的线程id和新线程的线程id是不同的,而且是地址,可以发现参数zebra成功传递到了thread_run函数中。

4、线程等待 pthread_join(主线程等待新线程)

  一般而言,线程也是需要被等待的,如果不等待,可能会导致类似于"僵尸进程"的问题 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。

  • 功能:等待线程结束,调用该函数的线程将挂起等待,直到id为thread的线程终止(也就是主线程阻塞式等待id为thread的进程终止)
  • 函数签名:int pthread_join(pthread_t thread, void **value_ptr);
  • 参数说明
    • thread:线程ID
    • value_ptr:是一个输出型参数,返回线程执行完函数后的返回值(这个函数的返回值也就是这个线程的返回值),我们创建函数的时候,传入的线程执行函数返回值是void*的,所以这里定义一个指向void*类型变量的指针。
  • 返回值:成功返回0;失败返回错误码

  列如:主线程等待其他线程执行完毕后才能继续执行(只能用这种for循环的方式,一个个等)

  注:如果在c++中,虽然可以把int转换成void*类型,但是无法将void*转回int,会报错,因为你是64位机器,void*是8字节,int是4字节,会有精度丢失,c++不允许这种精度丢失,c语言允许。

  所以如果在c++中,如果我们要使用void*类型的8个字节存储实际内容(void*类型本身就可以充当容器),我们需要使用long,将void*转成long类型,然后再转回去,不会有精度丢失,因为long在64位下是8字节

代码举例(使用void*存储参数的值)

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

const int NUM = 10;

void *run(void *param) {
    // int data = *(int*)param;  // 如果在c++中,虽然可以把int转换成void*类型,但是无法将void*转回int,会报错,因为你是64位机器,void*是8字节,int是4字节,会有精度丢失,c++不允许这种精度丢失,c语言允许
    // 所以如果在c++中,我们需要使用long,将void*转成long类型,然后再转回去,不会有精度丢失,因为long在64位下是8字节
    long data = (long)param;
    printf("current thread is %d\n",data);
    return (void*)data;
}

int main()
{
    pthread_t tid[NUM];
    for (int i = 0; i < NUM; i++) {
        pthread_create(&tid[i],NULL,run,(void*)i);
        usleep(100000);
    }
    void *status = NULL;
    int ret = 0;
    for(int i = 0; i < NUM; i++){
        ret = pthread_join(tid[i],&status);
        printf("ret: %d, status: %d\n", ret, (long)status);
    }
    return 0;
}

  输出结果

image

1、将参数i转换成void*类型,传到 run 方法内部,然后强转成 long 后,可以成功获取到参数 i 的值。

2、pthread_join函数可以等待线程,并通过一个输出型参数获取函数的返回值,也就是线程的返回值。

注意:创建线程执行完对应的函数后,有三种情况:

  • 代码跑完结果对

  • 代码跑完结果不对

  • 代码异常
    前两种pthread_join可以通过输出型参数status来判断代码结果是否正确,并作出处理 ,第三种代码异常,pthread_join不需要处理,处理异常是进程来做的。

线程终止的方案

  • 函数中return
    • main函数退出return的时候代表主线程and进程退出
    • 其他线程函数return,只代表当前线程退出
  • 新线程通过pthread_exit终止自己(exit是终止进程,不要在其他线程中调用,如果你就想终止一个线程的话! !)
  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数

  • 功能:终止调用该函数的线程
  • 函数签名: void pthread_exit(void *value_ptr);
  • 参数说明
    • value_ptr:value_ptr不要指向一个局部变量,一个输入型参数。这个参数会作为线程退出后的返回值
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

  注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

  • 功能:取消一个执行中的线程,取消线程ID为传入参数的线程
  • 函数签名:int pthread_cancel(pthread_t thread);
  • 参数说明:
    • thread:线程ID(可以在创建的时候用变量把线程id保存下来,在这里就可以拿来用)
  • 返回值:成功返回0;失败返回错误码

  注意事项: 取消线程以后,线程退出的返回值是-1(对应的是PTHREAD_CANCELED宏)

线程分离pthread_detach函数

  默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

  如果不关心线程的返回值,join是一种负担,这个时候可以告诉系统,当线程退出时,自动释放线程资源线程分离,分离之后的线程不需要被join(不能被join)运行完毕之后,会自动释放Z状态的pcb

函数签名:int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());

pthread中的线程id与Linux内核中的轻量级线程id的区别

  我们看到的线程id是pthread库的线程id,不是Linux内核中的LWP(light weight process),pthread库的线程id是一个内存地址,而Linux内核中的LWP是一个数值。

  1、我们使用多线程需要用到pthread库,这是一个采用动态链接的共享库,加载到内存最后都放在栈和堆之间的共享区(共享内存,动态库都是放在这里的),内存中只有一份。(通过线程自己的页表映射到同一个物理空间)

  每个线程都要有运行时的临时数据,每个线程都要有自己的私有栈结构,描述线程的用户级控制块。

  2、每一个新线程创建的时候,都会在pthread库里创建一个pthread结构体(每个线程都使用虚拟地址空间-mm_struct,注意task_struct包括mm_struct,每个线程有一个task_struct也就是PCB,其内部的mm_struct里面的共享区里面存放有所有线程的pthread结构体),用来保存线程的相关信息(线程的用户级数据,线程的私有栈),有100个线程,那在共享区内部的pthread库里面就会有100个用来保存pthread信息的结构体。

  在这个pthread库中,如何快速找到对应的pthread结构体呢,只要拿到线程id就行,线程id就是pthread结构体的地址,只要拿到这个地址,我们就可以很轻松地找到pthread,获取线程运行时的用户级数据。

  3、用户层调用pthread_create创建一个线程的时候,会在共享区内部创建一个pthread结构体,保存线程的栈,用户级数据(临时数据)等信息;返回给用户的线程id就是这个pthread结构体的地址。同时,在Linux内核中,也要为线程创建对应的pcb(CPU调度由pcb说了算)。其中,也要创建一个与当前线程id对应的lwp(在pthread结构体里面保存lwp,内核的pcb里面保存线程id,也就是pthread结构体的地址)。

  用户级线程1:1式的和Linux内核中的轻量级线程对应,这就是Linux实现线程的方式。

参考