Linux内核学习之进程管理

1. 进程

1.1 基本概念

操作系统上运行的一个程序。

1.2 进程标志符

系统用一个数字来唯一标志进程,称为PID。

  • 当分配的PID到达最大值,则重新从0开始寻找最小的PID分配。
  • 通过修改/proc/sys/kernel/pid_max可以修改PID的最大值。

1.3 进程描述符

每个进程也有一个自己的唯一的描述符,这个数据结构非常复杂,包含非常多的字段,以下列出主要关系字段

字段数据结构意义
pid,tgidpid_t进程id和线程组id
stateint进程状态,运行态,可中断等待态,不可中断等待态,暂停态,跟踪态,僵尸态,僵尸撤销态
thread_infothread_info*指向线程描述符的指针
mmmm_struct*指向内存描述符的指针
ttytty_struct*指向关联终端描述符的指针
signalsignal_struct*指向接受到的信号的描述符
fsfs_struct*指向当前目录的文件描述符
threadthread_struct*存储线程相关寄存器
filesfiles_struct*指向打开的文件描述符链表
real_parenttask_struct*指向实际父进程描述符
parenttask_struct*指向当前父进程描述符,通常与real_parent一致,当被另一个进程以ptrace系统调用跟踪时,会变成跟踪进程
childrenlist_head孩子进程链
siblinglist_head兄弟进程链

1.4 线程描述符和内核堆栈

内核给每个进程都分配一个内核栈,用于进程运行再内核态时使用,其数据结构如下,在栈底放入了一个线程描述符。线程描述符中有个很重要的字段task,用于指向当前进程的描述符。该8KB的联合体被内核放在内存8KB地址对齐的地方,因此用户态的进程,如果需要获取当前进程描述符,方法很简单,就是将当前CPU的sp寄存器的值的低13位置0,得到的指针就指向线程描述符,再从进程描述符中取得进程描述符的地址。

#define THREAD_SIZE_ORDER   1
#d efine THREAD_SIZE     (PAGE_SIZE << THREAD_SIZE_ORDER)

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];  // 对应8kB
};

线程描述符、内堆栈和进程描述符的关系图如下:
请添加图片描述

2. 线程(轻量级进程)

在Linux中,最初是只有进程的概念的,而没有线程的概念,后来为了兼容POSIX标准的线程概念,引入了轻量级进程(light weight process,LWP)的概念,也就是线程,没错,也就是说,线程在Linux中原型也是进程。进程是地址空间上隔离的程序,而线程是共享地址空间的。

看下,例如如下例程:

编译:g++ -g -pthread -o main main.cpp

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

void* t1_entry(void* )
{
        int i=10;
        while(i--)
        {
                printf("thread 1 running\n");
                sleep(1);
        }
        return 0;
}

int main(void)
{

        printf("hello world\n");
        pthread_t t1;
        pthread_create(&t1, NULL, t1_entry, NULL);
        pthread_join(t1, NULL);
        printf("thread 1 finished\n");
        return 0;
}

对main进行调试,并运行几秒后按下ctrl+c暂停程序,然后打印全部堆栈,可以看到两个线程,每个线程中出现了三个数字,Thread 2、Thread 0x7ffff6eb9700 和 LWP 1743663,其中2是线程编号,或序号,调试用,0x7ffff6eb9700 是POSIX中的线程id,由POSIX提供的pthread库进行管理,而最后一个才是我们Linux内核中使用的、真正的进程id,两个线程,即两个轻量级进程,具有两个进程id,后续我们将轻量级进程的pid称为线程id,即tid

(gdb) thread apply all bt

Thread 2 (Thread 0x7ffff6eb9700 (LWP 1743663) "main"):
#0  0x00007ffff6fb3918 in nanosleep () from /lib64/libc.so.6
#1  0x00007ffff6fb381e in sleep () from /lib64/libc.so.6
#2  0x00000000004006ff in t1_entry () at main.cpp:11
#3  0x00007ffff7bb61ca in start_thread () from /lib64/libpthread.so.0
#4  0x00007ffff6ef3e73 in clone () from /lib64/libc.so.6

Thread 1 (Thread 0x7ffff7fe6740 (LWP 1743662) "main"):
#0  0x00007ffff7bb76cd in __pthread_timedjoin_ex () from /lib64/libpthread.so.0
#1  0x0000000000400746 in main () at main.cpp:22

3. 线程组(宏观进程)

这里说到进程,指的宏观意义上的进程(我把Linux内核中的进程称为微观意义上的进程),一个进程由多个线程组成,这多个线程称为一个线程组,当创建一个进程时,它至少创建一个线程,这第一个线程就是主线程,或称领头线程,它的pid(后续陈述,用大写的PID表示宏观进程ID,小写pid表示微观进程id)作为整个线程组的PID。

再来看一个例子:
还是之前的代码,将t1_entry稍做修改,利用pthread_setname_np设置一下进程的名字。
注意:虽然POSIX接口是跨unix平台的接口,但是不要再跨平台的代码中使用pthread_setname_np,因为pthread_setname_np不是可移植的,其命名最后的 np 就是表示 not portable,类似带此后缀的其他接口也是如此。
编译:g++ -g -pthread -o main main.cpp

void* t1_entry(void* )
{
        pthread_setname_np(pthread_self(), "thread 1"); // set thread name
        int i=10;
        while(i--)
        {
                printf("thread 1 running\n");
                sleep(1);
        }
        return 0;
}

然后gdb main调试程序,运行几秒后,ctrl+c暂停,然后另开一个终端界面运行以下命令

top -H -p \`pgrep main\` 
# -H 是展示线程信息,top默认是只展示进程,不展示线程的
# -p 只展示进程id等于后面值的进程,这里进程指的宏观进程
# `pgrep main` 是获取main进程的PID值

进入信息界面后,按 f 进入如下界面,添加需要显示的字段,找到 TGID 按空格将它设置为显示(前面有*号),然后按 → 键选中它,按 ↑ 键将它挪到PID前面,便于显示时和PID对比。

* PID     = Process Id             USED    = Res+Swap Size (KiB)
* USER    = Effective User Name    nsIPC   = IPC namespace Inode
* PR      = Priority               nsMNT   = MNT namespace Inode
* NI      = Nice Value             nsNET   = NET namespace Inode
* VIRT    = Virtual Image (KiB)    nsPID   = PID namespace Inode
* RES     = Resident Size (KiB)    nsUSER  = USER namespace Inode
* SHR     = Shared Memory (KiB)    nsUTS   = UTS namespace Inode
* S       = Process Status         LXC     = LXC container name
* %CPU    = CPU Usage              RSan    = RES Anonymous (KiB)
* %MEM    = Memory Usage (RES)     RSfd    = RES File-based (KiB)
* TIME+   = CPU Time, hundredths   RSlk    = RES Locked (KiB)
* COMMAND = Command Name/Line      RSsh    = RES Shared (KiB)
  TGID    = Thread Group Id        CGNAME  = Control Group name

最后按esc退出 f 界面,输出结果如下:

top - 07:56:45 up 33 days,  4:41,  3 users,  load average: 0.04, 0.07, 0.02
Threads:   2 total,   0 running,   0 sleeping,   2 stopped,   0 zombie
%Cpu(s):  0.2 us,  0.3 sy,  0.0 ni, 96.4 id,  0.0 wa,  0.1 hi,  0.1 si,  2.9 st
MiB Mem :  15830.9 total,    877.2 free,   3809.6 used,  11144.0 buff/cache
MiB Swap:   8076.0 total,   8029.4 free,     46.6 used.  10939.7 avail Mem

   TGID     PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
1752235 1752235 root      20   0   24160   2184   2000 t   0.0   0.0   0:00.00 main
1752235 1752238 root      20   0   24160   2184   2000 t   0.0   0.0   0:00.00 thread 1

可以看到,main进程有两个线程,其tgid都是1752235,但是pid不同。

  1. c/c++编码过程中,可以通过原始系统调用拿到线程的原始pid,注意是原始系统调用接口(syscall),而不是c库封装了一层的getpid,getpid这个系统调用只能拿到进程的tgid,因此它是指宏观意义上的PID。

  2. vfork能创建共享地址空间的进程,但是和pthread_create创建的线程不同,虽然都是共享地址空间,咋一看作用是一样的,其实还是有不同的,最主要的,vfork创建的新进程,拥有新的tgid,而pthread_create创建的轻量级进程,仍共享旧的tgid。根据POSIX信号规范,信号会作用与线程组所有线程,换句话说,你给线程组里任何一个pthread_create创建的线程发送结束信号,那么整个线程组都会终止,比如给之前例子中的thread 1 发送结束信号kill 1752238,结果进程如下结束了。但是给vfork创建的进程发送结束信号,只会结束那一个进程。

[root@localhost ~]# ./main
hello world
thread 1 running
thread 1 running
thread 1 running
thread 1 running
thread 1 running
Terminated
  1. Linux原生创建进程的函数不是fork/vfork(这是规范接口),而是clone,clone非常灵活,包括线程创建的原生函数也是clone,如果你那天发现fork/vfork/pthread_create满足不了你的需求,同时你需求没有跨平台要求,可以试试clone。

4. 进程组

进程组由多个进程组成,同一个进程创建的子进程默认都属于同一个进程组(你可以通过setpgid修改),进程组中,第一个创建的进程的pid即进程组的id(pgid)。
在命令行下,我们每敲下的一行命令,shell都会为它创建一个新的进程组。
比如我们敲下:tail -f .bashrc | grep --color=auto 123 | grep --color=auto 456 &
bash会自动创建3个进程(1个tail,2个grep),并为他们新建一个进程组id。

top - 02:29:27 up 34 days, 23:14,  3 users,  load average: 0.07, 0.05, 0.00
Tasks:   3 total,   0 running,   3 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.2 us,  0.4 sy,  0.0 ni, 97.1 id,  0.0 wa,  0.1 hi,  0.0 si,  2.2 st
MiB Mem :  15830.9 total,    766.0 free,   3820.3 used,  11244.6 buff/cache
MiB Swap:   8076.0 total,   8029.4 free,     46.6 used.  10921.1 avail Mem

   PGRP     PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2639721 2639721 root      20   0    7360    936    864 S   0.0   0.0   0:00.00 tail
2639721 2639722 root      20   0   12144   1112   1016 S   0.0   0.0   0:00.00 grep
2639721 2639723 root      20   0   12144   1108   1012 S   0.0   0.0   0:00.00 grep

5. 会话

有时候,我们把进程组称为一项工作(job),那么会话就可以理解成一组工作,会话中,第一个创建的进程组的pgid即整个会话的id(sid),对于bash作为登录shell,第一个进程组即bash,它的pgid则是本次登录会话的sid。
一个登录会话下可以有很多工作,主要分为前台工作和后台工作,前台工作只有一个,后台可以有多个,通过fg命令将后台的命令放到前台执行,bg可以将挂起的进程放到后台执行(例如,你通过ctrl+z挂起了很多前台进程)

[root@localhost ~] tail | echo  # 启动第一个工作
^Z		# ctrl+z
[1]+  Stopped                 tail
[root@localhost ~] tail &		# 启动第二个工作
[2] 1785608
[root@localhost ~] sleep 20 &	# 启动第三个工作
[3] 1785729
[root@localhost ~] jobs
[1]-  Stopped                 tail | echo
[2]+  Stopped                 tail
[3]   Running                 sleep 20 &

6. Linux进程链表

进程管理主要通过进程描述符这个数据结构实现,每个进程描述符包含一个pids的数组,它是个数组,包含4个pid结构体,前面讨论过4种id(tid、tgid、pgid、sid),他们分别对应下表0~3,每个pid结构体包含三个部分,分别是id值和两个双向链表(节点)。Linux通过hash链表对这些双向链表进行管理,hash链表也有4个,每个链表的下标是对应id的hash值,哈希链表的list1即其拉链,链上每个节点其id值都不同,但是hash值相同。对于list2链表,则是Linux用于管理线程组、进程组和会话的,同一条list2链上的节点,具有相同的id值,如果是具有相同tgid值,表示他们属于同一线程组,同理可知pgid和sid。
在这里插入图片描述

进程亲属关系链表:
请添加图片描述

7. 进程创建

  1. 把当前进程描述符拷贝一份,然后修改其中部分内容。
  2. 申请PID,修改到进程描述符。
  3. 配置线程组、进程组、会话关系链。
  4. 复制内核态文件描述符、信号量、内存描述符。
  5. 将进程描述符挂载到运行队列(注册到调度系统)。

8. 进程切换

  1. 换页表,即保存cr3到前一进程描述符,将后一进程描述符的值恢复到cr3。
  2. 换任务状态段(TSS),这一步是相对x86架构而言的,x86在进中断时,会用TSS中的sp字段值加载到sp寄存器,退出时还原(具体参考这篇文章),Linux用到了这一机制来切换内核态堆栈,不同的进程使用不同的内核栈,但是由于Linux只为每个CPU维护一个TSS,而不是每进程,因此,在进程切换时,需要更新TSS中相关寄存器字段的值,将TSS段中部分寄存器内容保存到前一进程描述符,将后一进程描述符的TSS的寄存器内容恢复到TSS段中。
  3. 换线程局部段(TLS),Linux为每个CPU维护3个线程局部段,供用户程序使用,由于同样不是每进程的变量,因此需要更新。将当前全局描述符表中的三个TLS保存到当前进程描述符,将下一进程描述符中的该字段恢复回去。
  4. 换硬件寄存器,包括ip、sp、eflags、fs、g等 。将这些寄存器保存到前一进程描述符,将后一进程描述符保存的返回地址恢复到IP寄存器。

ps: 实际上,第5步后,只是从进程1的内核态切换到了进程2的内核态,当进程2从内核态返回时,会恢复用户态硬件上下文。

9. 内核线程

内核线程用于完成一些周期性的内核任务,如内存脏数据刷写到磁盘,维护网络连接,内存交换等等。

  1. 进程0,所有进程的祖先进程,是Linux创建的第一个进程,每个CPU核都有一个0号进程,进程0的信息无法在用户态看到,也就是ps等命令无法显示,但是可以看到进程1的父进程是0进程。
  2. 进程1,即所有用户态进程(包括内核线程)的祖先进程,在centos上一般是initd或systemd。
  3. 内核态线程,仅运行在内核态,对于4GB内存机器,它仅访问高1GB的地址空间。
  4. 创建内核线程,kernel_thread,属于内核符号,对用户态不可见,可在驱动程序中获取,详细方法参考我另一篇文件《hook系统调用》。
  5. 内核线程多以字母k开头。
root           2       0  0 Sep20 ?        00:00:04 [kthreadd]
root           6       2  0 Sep20 ?        00:00:00 [kworker/0:0H-events_highpri]
root           9       2  0 Sep20 ?        00:00:17 [ksoftirqd/0]
root          17       2  0 Sep20 ?        00:00:20 [ksoftirqd/1]
root          19       2  0 Sep20 ?        00:00:00 [kworker/1:0H-events_highpri]
root          23       2  0 Sep20 ?        00:00:20 [ksoftirqd/2]
root          25       2  0 Sep20 ?        00:00:00 [kworker/2:0H-events_highpri]
root          29       2  0 Sep20 ?        00:00:19 [ksoftirqd/3]
root          31       2  0 Sep20 ?        00:00:00 [kworker/3:0H-events_highpri]
root          35       2  0 Sep20 ?        00:00:19 [ksoftirqd/4]
root          37       2  0 Sep20 ?        00:00:00 [kworker/4:0H-events_highpri]
root          41       2  0 Sep20 ?        00:00:18 [ksoftirqd/5]
root          43       2  0 Sep20 ?        00:00:00 [kworker/5:0H-events_highpri]
root          47       2  0 Sep20 ?        00:00:16 [ksoftirqd/6]
root          49       2  0 Sep20 ?        00:00:00 [kworker/6:0H-events_highpri]
root          53       2  0 Sep20 ?        00:00:16 [ksoftirqd/7]
root          55       2  0 Sep20 ?        00:00:00 [kworker/7:0H-events_highpri]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux 内核通过进程管理子系统来管理进程进程管理子系统负责创建、调度和终止进程,并提供了一些系统调用和文件系统接口,使用户空间的程序可以与内核进行交互。 以下是 Linux 内核进行进程管理的一些关键技术: 1. 进程描述符:在 Linux 内核中,每个进程都有一个进程描述符(Process Descriptor,简称为 task 或进程控制块 PCB),它包含了进程的所有信息,比如进程状态、进程 ID、进程运行时间、进程优先级等。内核通过进程描述符来管理进程。 2. 进程调度器:进程调度器负责决定哪个进程应该获得 CPU 时间片并执行。Linux 内核中有多种进程调度器可供选择,比如 Completely Fair Scheduler(CFS)和 Realtime Scheduler(RT)等。 3. 进程通信:在 Linux 内核中,进程可以通过多种方式进行通信,比如管道、共享内存、信号量等。这些机制允许进程之间进行数据交换和同步操作。 4. 进程状态:在 Linux 内核中,进程可以处于不同的状态,比如运行态、就绪态、阻塞态等。内核通过状态转换来管理进程的生命周期。 5. 进程创建和终止:Linux 内核通过 fork() 和 exec() 等系统调用来创建新进程,通过 exit() 系统调用来终止进程。 总之,Linux 内核通过进程管理子系统来管理进程,包括进程描述符、进程调度器、进程通信、进程状态、进程创建和终止等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值