5.进程线程模型你知道多少?

发布时间 2023-09-16 19:12:32作者: CodeMagicianT

5.进程线程模型你知道多少?

1.进程


  1. 进程创建与结束

    • 背景知识:

      进程有两种创建方式,一种是操作系统创建的一种是父进程创建的。从计算机启动到终端执行程序的过程为:0号进程 -> 1号内核进程 -> 1号用户进程(init进程) -> getty进程 -> shell进程 -> 命令行执行进程。所以我们在命令行中通过 ./program执行可执行文件时,所有创建的进程都是shell进程的子进程,这也就是为什么shell一关闭,在shell中执行的进程都自动被关闭的原因。从shell进程到创建其他子进程需要通过以下接口。

    • 相关接口:

      • 创建进程:pid_t fork(void);

        返回值:出错返回-1;父进程中返回pid > 0;子进程中pid == 0

      • 结束进程:void exit(int status);

        • status是退出状态,保存在全局变量中S?,通常0表示正常退出。
      • 获得PID:pid_t getpid(void);

        返回调用者pid。

      • 获得父进程PID:pid_t getppid(void);

        返回父进程pid。

    • 其他补充:

      • 正常退出方式:exit()、_exit()、return(在main中)。

        exit()和_exit()区别:exit()是对__exit()的封装,都会终止进程并做相关收尾工作,最主要的区别是_exit()函数关闭全部描述符和清理函数后不会刷新流,但是exit()会在调用_exit()函数前刷新数据流。

        return和exit()区别:exit()是函数,但有参数,执行完之后控制权交给系统。return若是在调用函数中,执行完之后控制权交给调用进程,若是在main函数中,控制权交给系统。

      • 异常退出方式:abort()、终止信号。

  2. Linux进程控制

  • 进程地址空间(地址空间)

    虚拟存储器为每个进程提供了独占系统地址空间的假象。

    尽管每个进程地址空间内容不尽相同,但是他们的都有相似的结构。X86 Linux进程的地址空间底部是保留给用户程序的,包括文本、数据、堆、栈等,其中文本区和数据区是通过存储器映射方式将磁盘中可执行文件的相应段映射至虚拟存储器地址空间中。

    有一些"敏感"的地址需要注意下,对于32位进程来说,代码段从0x08048000开始。从0xC0000000开始到0xFFFFFFFF是内核地址空间,通常情况下代码运行在用户态(使用0x00000000 ~ 0xC00000000的用户地址空间),当发生系统调用、进程切换等操作时CPU控制寄存器设置模式位,进入内和模式,在该状态(超级用户模式)下进程可以访问全部存储器位置和执行全部指令。

    也就说32位进程的地址空间都是4G,但用户态下只能访问低3G的地址空间,若要访问3G ~ 4G的地址空间则只有进入内核态才行。

  • 进程控制块(处理机)

    进程的调度实际就是内核选择相应的进程控制块,被选择的进程控制块中包含了一个进程基本的信息。

  • 上下文切换

    内核管理所有进程控制块,而进程控制块记录了进程全部状态信息。每一次进程调度就是一次上下文切换,所谓的上下文本质上就是当前运行状态,主要包括通用寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈和内核数据结构(页表、进程表、文件表)等。

    进程执行时刻,内核可以决定抢占当前进程并开始新的进程,这个过程由内核调度器完成,当调度器选择了某个进程时称为该进程被调度,该过程通过上下文切换来改变当前状态。

    一次完整的上下文切换通常是进程原先运行于用户态,之后因系统调用或时间片到切换到内核态执行内核指令,完成上下文切换后回到用户态,此时已经切换到进程B。

2.线程

对于线程,我认为弄清以下两点非常重要:

  • 线程之间有无先后访问顺序(线程依赖关系)
  • 多个线程共享访问同一变量(同步互斥问题)
  1. 线程创建和结束

    • 背景知识:

      在一个文件内的多个函数通常都是按照main函数中出现的顺序来执行,但是在分时系统下,我们可以让每个函数都作为一个逻辑流并发执行,最简单的方式就是采用多线程策略。在main函数中调用多线程接口创建线程,每个线程对应特定的函数(操作),这样就可以不按照main函数中各个函数出现的顺序来执行,避免了忙等的情况。线程基本操作的接口如下。

    • 相关接口:

      • 创建线程:int pthread_create(pthread_t *tidp,const pthread_attr_t attr, void (start_rtn)(void),void *arg);

        创建一个新线程,pthread和start_routine不可或缺,分别用于标识线程和执行体入口,其他可以填NULL。

        • pthread:用来返回线程的tid,*pthread值即为tid,类型pthread_t == unsigned long int。
        • attr:指向线程属性结构体的指针,用于改变所创线程的属性,填NULL使用默认值。
        • start_routine:线程执行函数的首地址,传入函数指针。
        • arg:通过地址传递来传递函数参数,这里是无符号类型指针,可以传任意类型变量的地址,在被传入函数中先强制类型转换成所需类型即可。
      • 获得线程ID:pthread_t pthread_self();

        调用时,会打印线程ID。

      • 等待线程结束:int pthread_join(pthread_t tid, void** retval);

        主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。

        • tid:创建线程时通过指针得到tid值。
        • retval:指向返回值的指针。
      • 结束线程:pthread_exit(void *retval);

        子线程执行,用来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。

        • retval:同上。
      • 分离线程:int pthread_detach(pthread_t tid);

        主线程、子线程均可调用。主线程中pthread_detach(tid),子线程中pthread_detach(pthread_self()),调用后和主线程分离,子线程结束时自己立即回收资源。

        • tid:同上。
  2. 线程属性值修改

    • 背景知识:

      线程属性对象类型为pthread_attr_t,结构体定义如下:

typedef struct{
    int detachstate;    // 线程分离的状态
    int schedpolicy;    // 线程调度策略
    struct sched_param schedparam;    // 线程的调度参数
    int inheritsched;    // 线程的继承性
    int scope;    // 线程的作用域
    // 以下为线程栈的设置
    size_t guardsize;    // 线程栈末尾警戒缓冲大小
    int stackaddr_set;    // 线程的栈设置
    void *    stackaddr;    // 线程栈的位置
    size_t stacksize;    // 线程栈大小
}pthread_attr_t;
  • 相关接口:

    对上述结构体中各参数大多有:pthread_attr_get()和pthread_attr_set()系统调用函数来设置和获取。这里不一一罗列。