操作系统: 进程和线程

进程概念

  • 进程是执行的程序。

  • 进程不止是程序代码,程序代码有时称为文本段(text section)(或代码段 (code section))。进程还包括当前活动,如程序计数器(Program Counter, PC) 的值和处理器寄存器的内容等。另外进程通常还包括:进程堆栈(stack)(包括临时数据,如函数参数、返回地址和局部变量)和数据段(data section)(包括全局变量)。进程还可能包括堆(heap),这是进程运行时动态分配的内存。

    内存中的进程

  • 随着进程的运行,进程的状态可能会发生改变。进程的状态可能包括:创建、运行、就绪、等待、终止。

  • 在操作系统内,进程可以用它的**进程控制块(PCB)**来表示。每个进程控制块包括:进程状态、程序计数器(指定进程要执行的下条指令地址)、CPU 寄存器、CPU 调度信息、内存管理信息、统计信息、I/O 状态信息。

PCB

进程调度

  • 当进程不执行时,进程处于某个等待队列。操作系统有两种主要队列:I/O 请求队列和就绪队列。就绪队列包括所有准备执行并等待执行 CPU 的进程。
  • 长期调度程序或作业调度程序从大容量存储设备的缓冲池中选择进程加到内存,以便执行。短期调度程序或 CPU 调度程序从准备执行的进程中选择进程,并分配CPU。二者的区别主要体现在执行的频率不同。
  • 有些操作系统如分时系统,可能引入一个额外的中期调度程序,可将进程从内存中换出,从而降低多道程序程度。
  • 将 CPU 切换到另一个进程需要保存当前进程状态并恢复另一个进程的状态,此任务称为 上下文切换(context switch)

进程运行

进程创建与执行

  • 进程在其执行过程中能通过创建进程系统调用(create-process system call)创建多个新进程。创建进程称为 父(parent)进程,被创建的新进程称为 子(children)进程 。每个新进程可以再创建其他进程以形成 进程树(tree)

  • 多数操作系统根据一个唯一的 进程标识符(process identifier,pid) 来识别进程,通常是整数值。

  • 一个进程创建子进程时,子进程可能通过以下方式获取资源:

    • 从操作系统直接获取资源
    • 共享父进程资源
  • 父进程与子进程存在两种执行关系:

    • 父进程和子进程并发执行
    • 父进程等待,直到某个或全部子进程执行完
  • 新进程的地址空间有两种可能

    • 子进程时父进程的复制品
    • 子进程装入另一个新程序
  • 在 UNIX 操作系统中,通过 fork() 系统调用来创建新进程,新进程的地址空间复制了原来进程的地址空间。这种机制允许父进程与子进程轻松通信。通常,在系统调用 fork() 后,有个进程使用系统调用 exec(),以用新程序来取代进程的内存空间并开始执行。这种方式使得两个进程能相互通信,且按各自方式运行。如果父进程在子进程运行时没什么可做,那么它可以采用系统调用 wait() 把自己移出就绪队列,直到子进程终止。

Linux 进程生成

  • Windows 的进程创建采用 CreateProcess(), 类似于 fork(),不过,fork() 让子进程继承父进程的地址空间,而 CreateProcess() 需要加载到子进程的地址空间。

进程终止

  • 进程完成执行最后的语句并使用系统调用 exit() 时进程终止,此时进程可以返回状态值(通常为整数)给父进程(通过 wait() 获取),所有进程资源也会被操作系统释放。
  • 进程可以通过系统调用终止另一个进程,通常只有被终止进程的父进程才能执行该操作。
  • 有的系统中,在父进程退出时,其所有子进程也终止,此现象称为级联终止

进程间通信

  • 操作系统内并发进行的进程可以为独立进程或协作进程。如果一个进程不能影响其他进程,也不能被其他进程所影响,则改进程时独立的,否则改进程是协作的。

  • 进程协作需要进程间通信机制来允许进程相互交换数据,其包括两种基本模式:

    • 共享内存: 通信进程需要建立内存共享区域。共享内存方法要求,通信进程共享一些变量,进程通过使用这些共享变量来交换信息;共享内存系统,提供通信的责任主要在应用程序员上。
    • 消息传递:在分布式环境中非常有用,通信进程之间必须有通信线路。消息系统方法允许进程交换信息,提供通信的责任主要在应用程序员上。对于直接通信,采用对称寻址,每个进程必须明确地命名通信的接收者或发送者;间接通信中,通过 邮箱(mailboxes) 或 端口(ports) 来发送和接收消息,每个邮箱都有唯一的标识符,两个进程仅在其共享至少一个邮箱时可相互通信,一个进程可通过许多不同的邮箱和其他进程通信。

    进程间通信

客户机/服务器通信

  • 以上两种进程间通信技术能够用于客户机/服务器系统的通信。
  • 客户机/服务器系统还包括其他三种通信策略:套接字、远程过程调用和管道。
  • 套接字虽然通用和高效,但是在分布式进程之间属于一种低级形式的通信。一个原因是,套接字只允许在通信线程之间交换无结构的字节流。客户机或服务器程序需要自己加上数据结构。
  • 远程过程调用 (Remote Procedure Call, RPC) 对于通过网络连接系统之间的过程调用进行了抽象。远程过程调用需要解决主机大小端的数据表示问题,且要解决正好调用一次的调用语义。
  • 管道 (pipe) 是一条在进程间以字节流方式传送数据的通信通道,有 OS 核心的缓冲区(通常几十KB)来实现,只能单向传输。
  • 管道分为普通管道(又叫匿名管道)和命名管道,匿名管道允许一对进程通信,而且只有当进程相互通信时,普通管道才存在。命名管道通信可以是双向的(但是是半双工的),而且通信进程之间的父子关系不是必需的,当建立一个命名管道后,多个进程都可用它通信。

线程概念

线程是进程内的控制流。线程是 CPU 使用的基本单元,由线程 ID、程序计数器、寄存器集合和堆栈组成。它与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。

线程的概念

多线程的优点包括:用户响应的改进、进程内资源的共享、经济和可扩展性的因素(如更有效地使用多个处理核)。

多线程模型

有两种方法提供线程支持:用户层的用户线程(user thread)和内核层的内核线程(kernal thread)。用户线程位于内核之上,它的管理无需内核支持;而内核线程由操作系统来直接支持与管理。几乎所有的现代操作系统都支持内核线程。用户线程对程序员是可见的,而对内核是未知的。通常。用户线程与内核线程相比,创建和管理要更快,因为它并不需要内核干预。

三种不同类型模型关联用户线程和内核线程。多对一模型将多个用户线程映射到一个内核线程;一对一模型将每个用户线程映射到一个对应内核线程;多对多模型将多个用户线程在同样(或更少数量)的内核线程之间切换。

线程库

线程库为程序员提供创建和管理线程的 API。实现线程库的主要方法有两种:

  • 在用户空间中提供一个没有内核支持的库。此库的所有代码和数据结构都位于用户空间。调用库中的一个函数只是导致用户空间中的一个本地函数调用,而非系统调用。
  • 实现由操作系统直接支持的内核级的一个库,库内的代码和数据结构位于内核空间。调用库中的一个 API 函数通常会导致对内核的系统调用。

常用的主要线程库有三个: POSIX Pthreads、Windows 线程和 Java 线程。

  • Pthread 创建和执行一个 C 程序样例:
#include <pthread.h>
#include <stdio.h>
int sum = 0; /* this data is shared by the thread(s) */
void *runner(void *param);  /* thread call this thread */
int main(int argc, char *argv[]){
    pthread_t tid;        /* the thread identifier */
    pthread_attr_t attr;  /* set of thread attributes */
    if (argc != 2){
        return -1;
    }
    if (atoi(argv[1]) < 0){
        return -1;
    }
    /*  get the default attributes */
    pthread_attr_init(&attr);
    // create the thread
    pthread_create(&tid, &attr, runner, argv[1]);
    // wait for the thread to exit
    pthread_join(tid, NULL);
    printf("sum = %d\n", sum);
}

/* The Thread will begin control in this function */
void *runner(void *param){
    int i, upper = atoi(param);
    for (i = 1; i <= upper; i++)
        sum += i;
    pthread_exit(0);
}
  • Windows 线程库创建线程的例程:
#include <windows.h>
#include <stdio.h>
DWORD sum = 0; /* this data is shared by the thread(s) */

/* The thread runs in this separate function */
DWORD WINAPI Summation(LPVOID Param){
    DWORD Upper = *(DWORD*)Param;
    for (DWORD i = 0; i <= Upper; i++){
        Sum += i;
    }
    return 0;
}

int main(int argc, char *argv[]){
    DWORD ThreadId;
    HANDLE ThreadHandle;
    int Param;
    if (argc != 2){
        return -1;
    }
    if ((Param = atoi(argv[1])) < 0){
        return -1;
    }

    /* Create the thread */
    ThreadHandle = CreateHandle(
        NULL,       // default security attributes
        0,          // default stack size
        Summation,  // thread function
        &Param,     // parameter to thread function
        0,          // default creation flags
        &ThreadId); // returns the thread identifier
    if (ThreadHandle != NULL){
        /* wait for the thread to finish */
        WaitForSingleObject(ThreadHandle, INFINITE);

        /* Close the thread handle */
        CloseHandle(ThreadHandle);
        printf("sum = %d\n", Sum);
    }
}

隐式多线程

除了采用线程库 API 来显式创建线程,也可以使用隐式线程,这种线程的创建和管理交由编译器和运行时库来完成。隐式线程方法包括:线程池、OpenMP 和 Grand Central Dispath 等。

如果允许所有的并发请求都通过新线程创建来处理,则没法限制系统内的并发执行线程的数量。无限制的线程可能耗尽系统资源,如 CPU 时间和内存。解决这一问题的一种方法是使用线程池(Thread Pool)。

线程池的主要思想是:在进程开始时创建一定数量的线程,并加到池中以等待工作。当服务器收到请求时,它会唤醒池内的一个线程(如果有可用线程),并将服务的请求传递给它。一旦线程完成服务,它会返回到池中再等待工作。如果池内没有可用线程,那么服务器会等待,直到有空线程为止。

线程池具有以下优点:

  • 用现有线程服务请求比等待创建一个线程更快
  • 线程池限制了何人时候可以使用的线程数量
  • 将要执行任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务

多线程问题

多线程程序为程序员带来了许多挑战,包括 fork() 和 exec() 系统调用的语义。其他问题包括信号处理、线程撤消、线程本地存储(Thread-Local Storage, TLS)和调度激活等。

TLS 与局部变量容易混淆。局部变量只有再单个函数调用时才可见;而 TLS 在多个函数调用时都可见。在某些方面,TLS 类似静态 (Static) 数据,不同的是,TLS 数据是每个线程特有的。

参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值