浅析 Linux 进程与线程

简介

进程与线程是所有的程序员都熟知的概念,简单来说进程是一个执行中的程序,而线程是进程中的一条执行路径。进程是操作系统中基本的抽象概念,本文介绍 Linux 中进程和线程的用法以及原理,包括创建、消亡等。

进程

创建与执行

Linux 中进程的创建与执行分为两个函数,分别是 forkexec,如下代码所示:

int main() {
    pid_t pid;
    if ((pid = fork() < 0) {
        printf("fork error\n");
    } else if (pid == 0) {
    // child
       if (execle("/home/work/bin/test1", "test1", NULL) < 0) {
            printf("exec error\n");
       }
    }
    // parent
    if (waitpid(pid, NULL) < 0) {
        printf("wait error\n");
    }
}

fork 从当前进程创建一个子进程,此函数返回两次,对于父进程而言,返回的是子进程的进程号,对于子进程而言返回 0。子进程是父进程的副本,拥有与父进程一样的数据空间、堆和栈的副本,并且共享代码段。

由于子进程通常是为了调用 exec 装载其它程序执行,所以 Linux 采用了写时拷贝技术,即数据段、堆和栈的副本并不会在 fork 之后就真的拷贝,只是将这些内存区域的访问权限变为只读,如果父子进程中有任一个要修改这些区域,才会修改对应的内存页生成新的副本,这样子是为了提高性能。

fork 之后父进程先执行还是子进程先执行是不确定的,所以如果要求父子进程进行同步,往往需要使用进程间通信。fork 之后子进程会继承父进程的很多东西,如:

  • 打开的文件
  • 实际用户 ID、组用户 ID 等
  • 进程组
  • 当前工作目录
  • 信号屏蔽和安排
  • ...

父子进程的区别在于:

  • 进程 ID 不同
  • 子进程不继承父进程的文件锁
  • 子进程的未处理信号集为空
  • ...

fork 之后,子进程可以执行不同的代码段,也可以使用 exec 函数执行其它的程序。

进程描述符

进程在运行的时候,除了加载程序,还会打开文件、占用一些资源,并且会进入睡眠等其它状态。操作系统为了支持进程的运行,必然有一个数据结构保存着这些东西。在 Linux 中,一个名为 task_struct 的结构保存了进程运行时的所有信息,称为进程描述符:

struct task_struct {
    unsigned long state;
    int prio;
    pid_t pid;
    ...
}

进程描述符完整描述了一个进程:打开的文件、进程的地址空间、挂起的信号以及进程的信号等。系统将所有的进程描述符放在一个双端循环列表中:

进程描述符具体存放在内存的哪里呢?在内核栈的末尾。众所周知,进程中占用的内存一部分是栈,主要用于函数调用,不过这里说的栈一般指的是用户空间的栈,其实进程还有内核栈。当进程调用系统调用的时候,进程陷入内核,此时内核代表进程执行某个操作,此时使用的是内核空间的栈。

进程状态

进程描述符中的 state 描述了进程当前的状态,有如下 5 种:

  1. TASK_RUNNING:进程是可执行的,此时进程要么是正在执行,要么是在运行队列中等待被调度
  2. TASK_INTERRUPTIBLE:进程正在睡眠(阻塞),等待条件达成。如果条件达成或者收到信号,进程会被唤醒并且进入可运行状态
  3. TASK_UNINTERRUPTIBLE:进程处于不可中断状态,就算信号也无法唤醒,这种状态用的比较少
  4. _TASK_TRACED:进程正在被其它进程追踪,通常是为了调试
  5. _TASK_STOPPED:进程停止运行,通常是接收到 SIGINT、SIGTSTP 信号的时候。

fork 与 vfork

在使用了写时拷贝后,fork 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。fork 为了创建一个进程到底做了什么呢?fork 其实调用了 clone,这是一个系统调用,通过给 clone 传递参数,表明父子进程需要共享的资源,clone 内部会调用 do_fork,而 do_fork 的主要逻辑在 copy_process 中,大致有以下几步:

  1. 为新进程创建一个内核栈以及 task_struct,此时它们的值与父进程相同
  2. 将 task_struct 中某些变量,如统计信息,设置为 0
  3. 将子进程状态设置为 TASK_UNINTERRUPTIBLE,保证它不会被投入运行
  4. 分配 pid
  5. 根据传递给 clone 的参数,拷贝或者共享打开的文件、文件系统信息、信号处理函数以及进程的地址空间等。
  6. 返回指向子进程的指针

除了 fork 之外,Linux 还有一个类似的函数 vfork。它的功能与 vfork 相同,子进程在父进程的地址空间运行。不过,父进程会阻塞,直到子进程退出或者执行 exec。需要注意的是,子进程不能向地址空间写入数据。如果子进程修改数据、进行函数调用或者没有调用 exec 那么会带来未知的结果。vforkfork 没有写时拷贝的技术时是有着性能优势,现在已经没有太大的意义。

退出

进程的运行终有退出的时候,有 8 种方式使进程终止,其中 5 中为正常终止:

  1. 从 main 返回
  2. 调用 exit
  3. 调用 _exit 或 _Exit
  4. 最后一个线程从其启动例程返回
  5. 从最后一个线程调用 pthread_exit

异常终止方式有 3 种:

  1. 调用 abort
  2. 接收到一个信号
  3. 最后一个线程对取消请求作出响应

exit 函数会执行标准 I/O 库的清理关闭操作:对所有打开的流调用 fclose 函数,所有缓冲中的数据会被冲洗,而 _exit 会直接陷入内核。看下面的代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    printf("line 1\n");
    printf("line 2"); // 没有换行符

    // exit(0)
    _exit(0);
}

其中第二行输出没有 \n,如果末尾调用的是 _exit,则只会输出 line 1,如果替换为 exit,则第二行 line 2 也会输出。

进程退出最终会执行到系统的 do_exit 函数,主要有以下步骤:

  1. 删除进程定时器
  2. 释放进程占用的页表
  3. 递减文件描述符的引用计数,如果某个引用计数为 0,则关闭文件
  4. 向父进程发信号,给子进程重新找养父,并且把进程状态设置为 EXIT_ZOMBIE
  5. 调度其它进程

此时,进程的大部分资源都被释放了,并且不会进入运行状态。不过还有些资源保持着,主要是 task_struct 结构。之所以要留着是给父进程提供信息,让父进程知道子进程的一些信息,如退出码等。

需要注意的是,如果父进程不进行任何操作,那么这些信息会一直保留在内存中,成为僵尸进程,占用系统资源,如下面的代码:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        exit(0);
    } else {
        sleep(10);
    }
}

父进程 fork 出子进程后,子进程立刻退出,而父进程则进入睡眠。运行程序,观察进程状态:

可以看到,第一行进程为父进程,状态为 S,表示其正在睡眠,而第二为子进程,状态为 Z,表示僵尸状态(zombie),因为此时子进程已经退出,然而 task_struct 还保存着,等待父进程来处理。

父进程如何处理?调用 wait 函数,正如本文第一段代码中所示。当父进程调用 wait 后,子进程的 task_struct 才被释放。

如果父进程先结束了呢?在父进程结束的时候,会为其子进程找新的父进程,一直往上找,最终成为 init 进程的子进程。init 子进程会负责调用 wait 释放子进程的遗留信息。

线程

上面介绍了 Linux 中的进程,那么线程又是怎么的?网上一些说法是,Linux 中并没有真正的内核线程,线程是以进程的方式实现的,只不过它们之间会共享内存。这种说法有一定道理,但并不完全准确。

Linux 中刚开始是不支持线程的,后来出现了线程库 LinuxThreads,不过它有很多问题,主要是与 POXIS 标准不兼容。自 Linux 2.6 以来,Linux 中使用的就是新的线程库,NPTL(Native POSIX Thread Library)。

NPTL 中线程的创建也是通过 clone 实现的,并且通过以下的参数表明了线程的特征:

CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | 
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM

部分参数的含义如下:

  • CLONE_VM:所有线程都共享同一个进程地址空间
  • CLONE_FILES:所有线程都共享进程的文件描述符列表
  • CLONE_THREAD:所有线程都共享同一个进程 ID 以及 父进程 ID

NPTL 所实现的线程库是 1:1 的从用户线程映射到内核线程,并且内核为了实现 POSIX 的线程标准也做了一些改动,比如对于信号的处理等。所以说 Linux 内核完全不区分进程和线程,甚至不知道线程的存在这种说法现在是不准确的。

线程间共享代码段、堆以及打开的文件等,线程私有的部分有以下内容:

  • 线程 ID
  • 寄存器
  • 错误码(errno)
  • 信号屏蔽
  • ...

总结

Linux 中进程与线程的使用是程序员必备的技能,而如果能了解一些实现的原理,则可以使用的更加得心应手。本文介绍了 Linux 中进程的创建、执行以及消亡等,对于线程的实现及其与进程的关系也进行了简单的说明。进程和线程还有更多的内容可以研究,如进程调度、进程以及线程间的通信等。

参考

  • 《UNIX 环境高级编程》
  • 《Linux 内核设计与实现》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容