进程原理以及系统调用
一、进程\进程生命周期
1.进程
操作系统作为硬件的使用层,提供使用硬件资源的能力,进程作为操作系统使用层,提供使用操作系统抽象出的资源层的能力。
进程:是指计算机中已运行的程序。进程本身不是基本的运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
Linux 内核把进程叫做任务(task),进程的虚拟地址空间可分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。
在 Linux 系统中,进程是资源分配的基本单位,而 ** 线程(Thread)** 是调度的基本运行单位。进程负责资源隔离,线程负责任务调度。
-
进程(Process)
- 进程是程序的一次执行实例,拥有独立的地址空间、文件描述符、环境变量等资源。
- 进程是操作系统进行资源分配的最小单位。
-
线程(Thread)
- 线程是进程内的一个执行单元,共享进程的资源(如内存、文件句柄等),但有独立的栈和寄存器上下文。
- 线程是操作系统调度的最小单位,内核通过调度线程实现多任务并发执行。
-
轻量级进程(LWP)
- Linux 内核通过 **clone ()** 系统调用实现线程,每个线程在内核中被视为一个轻量级进程(LWP)。
- LWP 具有独立的调度实体(如进程 ID、优先级等),但共享同一进程的资源。
在 Linux 系统中,线程(Thread) 和 轻量级进程(LWP,Lightweight Process) 密切相关,但存在关键区别。以下是对它们的详细解释、区别分析以及具体代码示例:
线程(Thread)与 LWP 的区别
特性 线程(用户级线程) 轻量级进程(LWP) 抽象层次 用户空间的抽象,由线程库(如 pthread
)管理内核中的调度实体,由内核直接管理 资源共享 共享进程的资源(内存、文件描述符等) 共享或独立,取决于 clone()
调用的参数调度实体 对应一个 LWP,由内核调度 内核调度的最小单位,拥有独立的调度参数 创建方式 通过 pthread_create()
创建通过 clone()
系统调用创建ID 管理 用户级线程 ID( pthread_t
)内核级线程 ID( TID
,通过gettid()
获取)可见性 用户空间可见 内核可见,可通过 ps -eLf
查看
使用案例与代码示例
1. 使用
pthread
创建线程(对应 LWP)#include <pthread.h> #include <stdio.h> #include <unistd.h> // 用于 gettid() void* thread_func(void* arg) { printf("用户线程 ID (pthread_t): %lu\n", (unsigned long)pthread_self()); printf("内核 LWP ID (TID): %d\n", gettid()); // 获取 LWP 的 TID return NULL; } int main() { pthread_t thread; pthread_create(&thread, NULL, thread_func, NULL); pthread_join(thread, NULL); return 0; }
2. 使用
clone()
直接创建 LWP#include <stdio.h> #include <stdlib.h> #include <sched.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define STACK_SIZE 1024 * 1024 // 子进程函数 int child_function(void *arg) { int *shared_variable = (int *)arg; // 对共享变量进行操作 for (int i = 0; i < 5; i++) { (*shared_variable)++; printf("Child LWP: shared_variable = %d\n", *shared_variable); sleep(1); } return 0; } int main() { // 共享资源 int shared_variable = 0; // 为子进程分配栈空间 char *stack = (char *)malloc(STACK_SIZE); if (stack == NULL) { perror("malloc"); return 1; } // 使用 clone 创建第一个 LWP pid_t pid1 = clone(child_function, stack + STACK_SIZE, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, &shared_variable); if (pid1 == -1) { perror("clone"); free(stack); return 1; } // 使用 clone 创建第二个 LWP pid_t pid2 = clone(child_function, stack + STACK_SIZE, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, &shared_variable); if (pid2 == -1) { perror("clone"); free(stack); return 1; } // 等待两个 LWP 结束 int status1, status2; waitpid(pid1, &status1, 0); waitpid(pid2, &status2, 0); // 输出最终的共享变量值 printf("Main process: Final shared_variable = %d\n", shared_variable); // 释放栈空间 free(stack); return 0; }
在 Linux 系统中,当用户通过 pthread_create
创建线程时,底层确实是通过 clone()
系统调用创建了一个轻量级进程(LWP)。这是现代 Linux 线程实现(如 NPTL,Native POSIX Threads Library)的核心机制。
1. 底层机制:
pthread
如何通过clone()
创建 LWP关键点
pthread
与内核的关系:
Linux 的
pthread
库(如 NPTL)是用户空间的线程库,但其实现依赖内核支持的 LWP。每个
pthread
线程对应一个内核中的 LWP,由内核直接调度(1:1 线程模型)。
clone()
系统调用的作用:
clone()
是 Linux 中创建进程或线程的底层接口,通过不同的参数控制资源共享。
pthread_create
在底层会调用clone()
,并设置以下关键标志:CLONE_VM // 共享地址空间(同一进程) CLONE_FILES // 共享文件描述符表 CLONE_SIGHAND // 共享信号处理程序 CLONE_THREAD // 标记为同一线程组的成员(共享 TGID)
这些标志使新创建的 LWP 与父线程共享资源,表现为“线程”而非独立进程。
验证方法
查看线程的 LWP ID(TID):
在代码中通过
gettid()
获取线程的内核级 ID(TID):#include <sys/syscall.h> pid_t tid = syscall(SYS_gettid);
运行程序后,使用
ps -eLf
查看进程的 LWP 列表:$ ps -eLf | grep <进程名>
跟踪
clone()
调用:
使用
strace
跟踪线程创建过程:$ strace -f ./your_program
观察输出中是否出现
clone()
调用,并检查其参数(如CLONE_VM
)。
2. 代码示例:验证
pthread
线程的 LWP 本质示例代码
#include <pthread.h> #include <stdio.h> #include <sys/syscall.h> // 获取 TID #include <unistd.h> // getpid() void* thread_func(void* arg) { printf("用户线程 ID (pthread_t): %lu\n", (unsigned long)pthread_self()); printf("内核 LWP ID (TID): %ld\n", syscall(SYS_gettid)); // 直接调用 gettid() printf("进程 PID: %d\n", getpid()); return NULL; } int main() { printf("主线程 PID: %d, TID: %ld\n", getpid(), syscall(SYS_gettid)); pthread_t thread; pthread_create(&thread, NULL, thread_func, NULL); pthread_join(thread, NULL); return 0; }
输出结果
主线程 PID: 1234, TID: 1234 用户线程 ID (pthread_t): 140123456789760 内核 LWP ID (TID): 1235 进程 PID: 1234
关键分析
主线程的 TID 和 PID 相同:在 Linux 中,主线程的 TID 等于进程 PID。
新线程的 TID 不同:新线程的 TID 是一个独立的内核 LWP ID,但 PID 仍与主线程相同(因为它们属于同一进程)。
3.
clone()
与fork()
的区别
特性 clone()
fork()
资源共享 通过参数灵活控制(如 CLONE_VM
)默认不共享资源(独立地址空间) 用途 创建线程或轻量级进程 创建独立进程 性能 更高(避免复制资源) 较低(需要复制页表等资源)
在 Linux 系统中,线程的设计确实完全基于 task_struct
结构体,并且通过共享特定资源的方式实现线程的轻量化。轻量级进程(LWP)正是这一设计思想的具体体现。
1.
task_struct
与线程/LWP 的关系Linux 内核中,所有执行实体(包括进程、线程、LWP)均由
task_struct
结构体表示。线程与普通进程的核心区别在于资源共享的粒度,而非底层数据结构的不同。
task_struct
中共享的资源当多个线程(LWP)属于同一进程时,它们的
task_struct
会共享以下关键资源:
共享资源 对应的 task_struct
字段作用 地址空间 struct mm_struct *mm
共享同一虚拟内存(代码段、堆、栈等) 文件描述符表 struct files_struct *files
共享打开的文件、套接字等 信号处理程序 struct signal_struct *signal
共享信号处理函数和信号屏蔽字 进程根目录 struct fs_struct *fs
共享当前工作目录和根目录 进程间通信资源 IPC 相关字段(如信号量、共享内存) 共享 IPC 对象
task_struct
中独立的部分每个线程(LWP)的
task_struct
中保留以下独立资源:
独立资源 对应的 task_struct
字段作用 线程栈 void *stack
独立的用户态栈空间 寄存器上下文 CPU 寄存器状态(保存于内核栈) 线程的独立执行状态 调度参数 优先级、调度策略、CPU 亲和性等 内核独立调度线程 线程 ID (TID) pid_t pid
(实际为内核 TID)内核唯一标识符
2. LWP 如何体现资源共享思想
LWP 的核心思想是通过
clone()
系统调用参数 控制资源共享。当创建线程时,clone()
指定共享标志(如CLONE_VM
),使得新 LWP 的task_struct
共享父进程的资源。关键标志与资源共享
以下是
clone()
的标志与task_struct
字段的对应关系:
clone()
标志共享的资源 对应的 task_struct
字段CLONE_VM
地址空间 mm
CLONE_FILES
文件描述符表 files
CLONE_FS
文件系统信息(根目录、工作目录) fs
CLONE_SIGHAND
信号处理程序 signal
CLONE_THREAD
共享线程组 ID(TGID) tgid
(所有线程的tgid
相同)
Linux通过:ps命令用于输出当前系统的进程状态。显示瞬间进程的状态,并不是动态连续;如果想要动态连续对进程监控就需要使用top命令。
2.进程的生命周期
Linux 操作系统属于多任务操作系统,系统中的每个进程能够分时复用 CPU 时间片,通过有效的进程调度策略实现多任务并行执行。而进程在被 CPU 调度运行,等待 CPU 资源分配以及等待外部事件时会属于不同的状态。进程状态如下:
创建状态:创建新进程;
就绪状态:进程获取可以运作所有资源及准备相关条件;
执行状态:进程正在 CPU 中执行操作;
阻塞状态:进程因等待某些资源而被跳出 CPU;
终止状态:进程消亡。
二、task_struct数据结构分析
进程是操作系统调度的一个实体,需要对进程所必须资源做抽象化,此抽象化为进程控制块(PCB,Process Control Block)。在 Linux 内核里采用 task_struct 结构体来描述进程控制块,Linux 内核涉及进程和程序的所有算法都围绕名为 task_struct 的数据结构建立操作。Linux源码示例如下:
以下是5个比较重要的成员:
1.
state
- 含义:用于表示进程的运行状态 ,是一个
volatile long
类型。常见取值及含义:
TASK_RUNNING
(值为 0):进程处于可运行状态,要么正在 CPU 上执行,要么在就绪队列中等待被调度执行。TASK_INTERRUPTIBLE
(值为 1):可中断的睡眠状态,进程因等待资源等条件而被挂起,当等待条件满足或收到信号时可被唤醒进入就绪态。TASK_UNINTERRUPTIBLE
(值为 2):不可中断的睡眠状态,进程等待特定资源,不响应信号,只有资源满足时才会被唤醒 。__task_stopped
(值为 4):进程被暂停执行,通常由信号(如SIGSTOP
等)导致 。__task_traced
(值为 8):进程正被调试器等进程监视 。- 示例:在调度器代码中,会检查进程的
state
值来决定是否将其投入运行。比如在每次调度时,调度器会遍历进程链表,寻找state
为TASK_RUNNING
的进程来分配 CPU 时间片。2.
pid
- 含义:类型为
pid_t
,是进程的唯一标识符,就像每个进程的 “身份证号”。系统通过pid
来区分不同进程,很多与进程交互的操作(如发送信号、获取进程状态等)都依赖于pid
。- 示例:当使用
kill
命令向进程发送信号时,就需要指定进程的pid
。在 C 语言代码中,也可以通过getpid()
函数获取当前进程的pid
,例如:#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t my_pid = getpid(); printf("My process ID is: %d\n", my_pid); return 0; }
3.
parent
- 含义:类型为
struct task_struct __rcu *
,指向该进程的父进程的task_struct
结构体。在进程树结构中,体现了进程间的父子关系。父进程创建子进程后,子进程的parent
指针指向父进程 。- 示例:在实现进程资源回收等功能时会用到。当子进程结束时,内核会通过子进程的
parent
指针找到其父进程,向父进程发送信号(如SIGCHLD
),通知父进程子进程已终止,父进程可进行相应处理(如调用wait
系列函数获取子进程退出状态)。4.
mm
- 含义:类型为
struct mm_struct *
,指向mm_struct
结构体,该结构体描述了进程的内存空间相关信息,包括进程的代码段、数据段、堆、栈以及映射的内存区域等。通过它,内核可以管理进程对内存的访问和使用 。- 示例:当进程执行内存分配操作(如调用
malloc
)时,内核会根据mm
结构体记录的信息,在进程的虚拟内存空间中合适的位置分配内存,并更新mm
结构体中相关字段(如堆的边界等信息) 。5.
priority
- 含义:表示进程的静态优先级,是一个
unsigned int
类型。优先级决定了进程获取 CPU 时间片的机会和时长等调度相关特性。数值越小,优先级越高 。- 示例:在调度算法中,内核会根据进程的
priority
值来决定调度顺序。例如在一些基于优先级的调度策略里,优先级高(priority
值小)的进程会优先被调度执行,并且可能分配到更长的时间片 。6.
se
- 含义:类型为
struct sched_entity
,struct sched_entity
是用于描述进程调度相关属性的结构体。在 CFS(完全公平调度器)中,vruntime
(虚拟运行时间)是一个核心要素,用于衡量进程应该运行的时间量。它记录在sched_entity
结构体里,通过不断累积进程运行过程中的虚拟时间来反映其对 CPU 时间的 “占用情况”。比如,优先级高的进程,其虚拟时钟走得慢,vruntime
增长慢,在调度时就更容易被选中运行。- 作用原理简述:CFS 调度器基于红黑树来管理处于就绪态的进程,红黑树的节点对应进程的
se
结构体。调度器每次选择vruntime
最小的进程投入运行,以此实现 CPU 时间的公平分配。当进程运行时,其vruntime
会根据运行时长和自身权重等因素不断更新;进程睡眠时,vruntime
保持不变。
三、进程优先级/系统调用
1、进程优先级
限期进程的优先级比实时进程要高,实时进程的优先级比普通进程要高。
▶ 限期进程的优先级是 - 1;
▶ 实时进程的优先级 1-99,优先级数值越大,表示优先级越高;
▶ 普通进程的静态优先级为 100-139,优先级数值越小,优先级越高,可通过修改 nice 值改变普通进程的优先级,优先级等于 120 加上 nice 值。
2.系统调用
当运行应用程序的时候,调用fork()/vfork()/clone()
函数就是系统调用。系统调用是应用程序进入内核空间执行任务的方式,程序通过系统调用执行创建进程、文件 IO 等一系列操作,具体如下图所示。
3.内核线程
内核线程是直接由内核本身启动的进程,实际上是将内核函数委托给独立的进程,与系统中其他进程 “并行” 执行(实际上,也并行于内核自身的执行),内核线程经常被称为(内核)守护进程,用于执行下列任务:
▶ 周期性地将修改的内存页与页来源块设备同步(例如,使用 mmap 的文件映射);
▶ 如果内存页很少使用,则写入交换区;
▶ 管理延时动作(deferred action);
▶ 实现文件系统的事务日志。
它是独立运行在内核空间的进程,与普通用户进程区别在于内核线程没有独立的进程地址空间。task_struct 数据结构里面有一个成员指针 mm 设置为 NULL,它只能运行在内核空间。
内核线程虽然没有独立的进程地址空间(其
task_struct
里的mm
指针通常为NULL
) ,但仍然需要分配内存空间用于执行任务,主要通过以下方式:
- 内核内存分配函数:
kmalloc
:用于在内核态分配连续的物理内存空间,分配的内存大小有一定限制(一般最大为 128KB )。它从预先分配好的内存池中获取内存,速度较快。例如,内核线程若需要分配小块内存用于存储临时数据结构等,可使用kmalloc
。其函数原型为void *kmalloc(size_t size, gfp_t flags)
,其中size
是待分配的内存大小,flags
用于控制分配行为,常用gfp_kernel
,表示内存不足时进程可睡眠等待内存 。vmalloc
:当需要分配较大的内存空间,且不要求物理地址连续时使用。它分配的是虚拟地址连续的内存,通过修改页表来映射物理内存,开销相对较大。适用于内核线程需要较大缓冲区等场景 。函数原型为void *vmalloc(unsigned long size)
。- 页分配函数:如
alloc_page
、alloc_pages
等。这些函数以页为单位分配内存(32 位机中一页通常为 4KB,64 位机中一页为 8KB )。alloc_page
用于分配单个页面,alloc_pages
可分配多个连续页面 。- 栈空间分配:内核线程有自己独立的栈空间,用于函数调用、存储局部变量等。栈空间大小通常由内核配置决定,一般为 8KB 或 16KB 。内核在创建内核线程时,会从内核虚拟地址空间顶部为其分配一块连续的内存区域作为栈空间 。
内核线程使用示例代码
下面是一个简单创建内核线程的示例代码,基于 Linux 内核提供的接口:
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kthread.h> // 内核线程执行的函数 static int my_kernel_thread_func(void *data) { // 这里写内核线程要执行的任务逻辑 // 示例:打印传入的数据 printk(KERN_INFO "My kernel thread is running, data: %p\n", data); // 模拟内核线程执行一段时间后退出 for (int i = 0; i < 10000000; i++) { // 简单空循环,消耗一些时间 } return 0; } static struct task_struct *my_thread; static int __init my_module_init(void) { // 创建内核线程 my_thread = kthread_create(my_kernel_thread_func, (void *)0x12345678, "my_kernel_thread"); if (IS_ERR(my_thread)) { printk(KERN_ERR "Failed to create kernel thread\n"); return PTR_ERR(my_thread); } // 启动内核线程 wake_up_process(my_thread); return 0; } static void __exit my_module_exit(void) { if (my_thread) { // 尝试停止内核线程 kthread_stop(my_thread); } } module_init(my_module_init); module_exit(my_module_exit); MODULE_AUTHOR("jerry"); MODULE_DESCRIPTION("A simple kernel thread example"); MODULE_LICENSE("GPL");
4.退出进程
退出进程有两种方式:一种是调用exit()系统调用或从某个程序主函数返回;另一个方式是接收到杀死信号或者异常时被终止。
https://github.com/0voice