进程

定义

  • 进程是资源CPU、内存等)分配的基本单位。
  • 进程是正在执行的一个程序或命令,每一个进程都是一个运行的实体,都有自己的地址空间,并占用一定的系统资源
  • 线程是系统的最小调度单位,一个进程可以拥有多个线程,同一进程里的线程可以共享进程间的同一资源。
  • 每个进程都有唯一的标识符,即进程ID,简称pid

进程间通信

  • 管道通信:有名管道,无名管道
  • 信号通信:信号的发送,信号的接收,信号的处理
  • IPC通信:共享内存,消息队列,信号量
  • Socket通信

分类

linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程

  • 交互式进程(interactive process):经常与用户进行交互, 因此需要花费很多时间等待键盘和鼠标操作. 当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝
  • shell, 文本编辑程序和图形应用程序批处理进程(batch process):不必与用户交互, 因此经常在后台运行.,因为这样的进程不必很快响应, 因此常受到调度程序的怠慢
  • 程序语言的编译程序, 数据库搜索引擎以及科学计算实时进程(real-time process):有很强的调度需要, 这样的进程绝不会被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短

linux中, 调度算法可以明确的确认所有实时进程的身份, 但是没办法区分交互式程序和批处理程序, linux2.6的调度程序实现了基于进程过去行为的启发式算法, 以确定进程应该被当做交互式进程还是批处理进程. 当然与批处理进程相比, 调度程序有偏爱交互式进程的倾向。

进程上下文

进程上下文是指操作系统在执行进程时所需的所有状态信息的集合

  • 包括程序的代码、数据、进程的标识符、堆栈、寄存器的值等。
  • 进程上下文的切换通常发生在操作系统的调度器决定切换到另一个进程运行时。

进程创建

  • 执行正在运行的进程所调用的进程创建系统调用fork()clone()
  • 系统初始化:所有的进程都是由其他进程创建(除了pid=0idle进程),pid=1init进程是系统启动后运行的第一个进程,是所有进程的父进程init进程会初始化一部分系统服务,创建其他进程

父子进程关系

  • 父进程创建一个新的进程时,该新进程就成为子进程。父进程在创建子进程时,会为子进程分配独立的资源和运行环境。
  • 子进程会继承父进程的大部分属性和资源。它可以独立运行,并且可以执行不同的代码路径。子进程可以创建自己的子进程,形成进程的层次结构

父子进程区别

进程ID、进程关系、资源继承、进程通信、生命周期

  • 进程ID:每个进程在系统中都有一个唯一的进程ID。父进程在创建子进程时,会将子进程的进程ID分配给子进程。
  • 进程关系:父进程与子进程之间建立了一种层次关系,父进程是子进程的创造者和管理者
  • 资源继承:子进程会继承父进程的大部分属性和资源,包括打开的文件、环境变量和当前工作目录等。
  • 进程通信:父进程和子进程可以通过进程间通信机制来进行交互和数据共享,如管道、共享内存、消息队列等。

继承情况

代码段,数据段,文件描述符

  • 父进程子进程拥有相同的代码段、数据段,有各自独立的地址空间,采用写时拷贝技术,在需要修改时才复制资源。
  • 父进程的信号、文件锁不会被子进程继承。
  • 子进程拥有父进程打开的文件描述符;在父进程中返回子进程PID,在子进程中返回0

pid

/**
 * @file
 * 包含进程相关操作的头文件。
 */
 
#ifndef _SYS_TYPES_H
#define _SYS_TYPES_H
 
// 其他系统类型定义...
 
#endif // _SYS_TYPES_H
 
/**
 * @file
 * 提供UNIX标准定义的系统调用和库函数。
 */
 
#ifndef _UNISTD_H
#define _UNISTD_H
 
#include <sys/types.h> // 包含系统类型定义
 
#ifdef __cplusplus
extern "C" {
#endif
 
// 其他UNIX标准函数...
 
/**
 * @brief 获取当前进程的PID。
 *
 * @return 当前进程的PID。
 */
pid_t getpid(void);
 
/**
 * @brief 获取当前进程的父进程PID。
 *
 * @return 父进程的PID。
 */
pid_t getppid(void);
 
/**
 * @brief 创建一个新的进程。
 *
 * fork函数复制调用它的进程以创建一个新的进程。新进程(子进程)将拥有与父进程相同的内存映像。
 * 子进程从fork()返回0,父进程返回子进程的PID。
 *
 * @return 在子进程中返回0,在父进程中返回子进程的PID,如果创建失败则返回-1。
 */
pid_t fork(void);
 
#ifdef __cplusplus
}
#endif
 
#endif // _UNISTD_H

execve()

  • fork函数创建子进程后,并不会加载程序。
  • 子进程往往要调用一种exec函数以执行另一个程序,该子进程被新的程序替换,改变地址空间,进程映像和一些属性,但是pid号不变。
/**
 * \file fs/exec.c
 *
 * Implementation of the execve system call and its variants.
 */
 
/**
 * Executes a new program by replacing the current process's memory image.
 *
 * This function is the kernel entry point for the execve system call.
 *
 * @param filename A user-space pointer to the name of the executable file.
 * @param argv A user-space array of pointers to the arguments.
 * @param envp A user-space array of pointers to the environment variables.
 * @return On success, the execve system call does not return. On error, it returns -1 and sets errno.
 */
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp)
{
	 return do_execve(getname(filename), argv, envp);
}
 
/**
 * Copies the user-space filename pointer to the kernel space and returns a struct filename.
 *
 * This function allocates a new filename structure and copies the user-space filename
 * to the kernel space.
 *
 * @param filename A user-space pointer to the filename.
 * @return A pointer to the new filename structure on success, or an error pointer on failure.
 */
struct filename *getname(const char __user *filename);
 
/**
 * Performs the actual execution of the new program.
 *
 * This function processes the arguments, sets up the new memory image, and starts the new program.
 *
 * @param filename A kernel-space pointer to the filename structure.
 * @param argv A user-space array of pointers to the arguments.
 * @param envp A user-space array of pointers to the environment variables.
 * @return 0 on success, or a negative error code on failure.
 */
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp);
 
/**
 * Handles the execveat system call and starts the execution of the new program.
 *
 * This function performs the actual file handling and execution of the new program.
 *
 * @param fd The file descriptor of the executable file.
 * @param filename A kernel-space pointer to the filename structure.
 * @param argv A user-space array of pointers to the arguments.
 * @param envp A user-space array of pointers to the environment variables.
 * @param flags Flags for the execveat system call.
 * @return 0 on success, or a negative error code on failure.
 */
int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags)
{
	struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

相关命令

ps kill

  1. 判断服务器健康状态
  2. 查看系统所有进程
  3. 杀死进程
命令格式功能常用参数示例
psps [参数]显示当前进程的状态auxps aux
killkill [选项] PID杀死进程-9SIGKILLkill -9 PID

进程状态符号说明

状态符号描述
D无法中断的休眠状态 (通常IO的进程)
R正在执行中
S静止状态
T暂停执行
Z不存在但暂时无法消除
W没有足够的记忆体分页可分配
<高优先序的行程
N低优先序的行程
L有记忆体分页分配并锁在记忆体内 (实时系统或I/O)

孤儿进程

父进程先于子进程结束运行,子进程成为孤儿进程并由init进程接管。

僵尸进程

子进程已经终止,但父进程尚未调用wait()waitpid()来获取子进程的终止状态,子进程进入僵尸进程状态。

正确处理僵尸进程的方法

  • 父进程应该及时使用wait()waitpid()系统调用来回收子进程的资源。
  • 同时,可以通过注册SIGCHLD信号的处理函数,在函数内部调用wait()waitpid()来处理子进程的终止状态,以避免僵尸进程的累积。

wait()

  • wait()函数一般用在父进程中等待回收子进程的资源,而防止僵尸进程的产生。
/**
 * @file
 * 包含等待子进程状态改变的函数声明。
 */
 
#ifndef _SYS_WAIT_H
#define _SYS_WAIT_H
 
#ifdef __cplusplus
extern "C" {
#endif
 
#include <sys/types.h> // 包含基本系统类型定义
 
/**
 * @brief 等待一个子进程状态改变。
 *
 * wait函数挂起调用进程,直到有一个子进程改变了状态。
 * 这个状态改变可以是子进程终止,或者子进程当前被停止或继续。
 * 如果指定了status参数,wait函数会将子进程的退出状态存储在status所指向的变量中。
 *
 * @param status 指向一个整数变量的指针,用于接收子进程的退出状态。
 *
 * @return 成功时返回回收的子进程的PID,失败时返回-1,并将errno设置为相应的错误代码。
 */
pid_t wait(int *status);
 
#ifdef __cplusplus
}
#endif
 
#endif // _SYS_WAIT_H
  • 相关的宏定义
WIFEXITED(status):如果子进程正常退出,则该宏定义为真
 
WEXITSTATUS(status):如果子进程正常退出,则该宏定义的值为子进程的退出值。

守护进程

定义

后台运行的特殊进程,通常以init进程为父进程,独立于终端或控制终端,用于执行常驻任务。

  • 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。

用户使守护进程独立于所有终端是因为,在守护进程从一个终端启动的情况下,这同一个终端可能被其他的用户使用。例如,用户从一个终端启动守护进程后退出,然后另外一个人也登录到这个终端。用户不希望后者在使用该终端的过程中,接收到守护进程的任何错误信息。同样,由终端键入的任何信号(例如中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。虽然让服务器后台运行很容易(只要shell命令行以&结尾即可),但用户还应该做些工作,让程序本身能够自动进入后台,且不依赖于任何终端。 守护进程没有控制终端,因此当某些情况发生时,不管是一般的报告性信息,还是需由管理员处理的紧急信息,都需要以某种方式输出。Syslog 函数就是输出这些信息的标准方法,它把信息发送给 syslogd 守护进程。

怎样创建守护进程

必须作为init进程的子进程 不与控制终端交互

步骤

  1. 创建子进程:使用fork函数创建一个新的进程。
  2. 终止父进程:父进程调用exit()函数或其他方式终止自身执行,从而使子进程成为孤儿进程
  3. 创建新会话:子进程调用setsid()函数创建一个新的会话。这将使子进程成为会话领导者,并且与其父进程和控制终端解除关系。
  4. 更改目录:调用chdir函数,将当前的工作目录改成根目录,避免后续操作与其他进程的目录关联。。(不是必须要的)
  5. 更新权限:守护进程会调用umask()函数来重设文件权限掩码,这样可以确保守护进程创建的文件具有适当的权限。(不是必须要的)
  6. 关闭文件描述符:节省资源,防止守护进程意外地与控制终端进行交互。(不是必须要的)
  7. 执行我们需要执行的代码(必须要的)

进程线程的状态转换图

三状态转换

  • 就绪状态:进程已获得除CPU外的所有必要资源,只等待CPU时的状态。一个系统会将多个处于就绪状态的进程排成一个就绪队列。
  • 执行状态:进程已获CPU,正在执行。单处理机系统中,处于执行状态的进程只一个;多处理机系统中,有多个处于执行状态的进程。
  • 阻塞状态:正在执行的进程由于某种原因而暂时无法继续执行,便放弃处理机而处于暂停状态,即进程执行受阻。(这种状态又称等待状态或封锁状态)

通常导致进程阻塞的典型事件有:请求I/O,申请缓冲空间等。 一般,将处于阻塞状态的进程排成一个队列,有的系统还根据阻塞原因不同把这些阻塞集成排成多个队列。

  • 就绪→执行:处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
  • 执行→就绪:处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
  • 执行→阻塞:正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
  • 阻塞→就绪:处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。

五状态转换⭐⭐⭐⭐

  • 创建状态:进程刚被创建,系统为其分配所需的资源,创建进程控制块(PCB)来管理进程的信息和状态。
  • 就绪状态:进程已经准备好开始执行,但还没有获取到处理器资源,处于等待调度的状态。
  • 执行状态:进程已经获取到处理器资源,正在执行指令和运行程序。
  • 阻塞状态:在执行状态下,如果进程遇到阻塞操作,例如等待I/O完成,它会进入阻塞状态。在此状态下,进程暂时无法继续执行,直到阻塞的操作完成或者条件满足后才能再次进入就绪状态。
  • 终止状态:进程执行结束或者被系统终止,进入终止状态。在终止状态下,进程的资源会被释放,PCB会被删除。

CPU工作原理⭐⭐

  • 取指令(Instruction Fetch):CPU 从内存中获取当前要执行的指令。CPU 会根据指令寄存器中的指令地址,将指令从内存中读取到指令缓存(Instruction Cache)中。
  • 解码指令(Instruction Decode):CPU 解析指令,确定指令的操作类型(如加载、存储、运算等),以及操作的操作数(如寄存器、内存地址等)。
  • 执行指令Execute):CPU 根据指令的操作类型和操作数执行相应的操作。这可能涉及数据的加载、存储、算术运算、逻辑运算、分支跳转等操作。
  • 访问内存(Memory Access):如果指令需要访问内存(如加载、存储操作),CPU 将计算出需要读取或写入的内存地址,并将数据从内存中读取或写入。
  • 写回结果(Write Back):如果执行的指令产生了结果,CPU 将结果写回到寄存器或内存中,以便后续的指令可以使用这些结果。

线程

线程是CPU调度和分配的基本单位(程序执行的最小单位)。

  • 线程之间的切换比进程之间的切换更快,因为线程共享相同的上下文和资源。
  • 线程是相互依赖的,一个线程的崩溃会导致整个进程的崩溃。

线程切换详细流程

  1. 上下文保存:线程切换时,首先保存上下文信息,保存在Thread Control Block中,包括寄存器状态、程序计数器、堆栈指针等,用于保存线程的执行状态。
  2. 切换到调度器:调度器(Scheduler)负责根据调度算法选择下一个要执行的线程。
  3. 上下文恢复:调度器选择了下一个要执行的线程后,从该线程的TCB中恢复线程的执行状态。
  4. 切换到新线程:调度器将执行权切换到新线程,使其开始执行。

线程内存占用

一个线程在Linux系统中大约占用8MB的内存。

Linux系统中的线程栈是通过缺页异常来进行内存分配的,不是所有的栈空间都会被实际分配内存。

如何降低线程崩溃的影响

  • 异常处理:使用异常捕获机制,确保单个线程的异常不会传播到其他线程。
  • 资源管理:限制单个线程可以使用的资源数量。
  • 错误恢复

创建线程

在C/C++中,可以使用POSIX线程库(pthread)来创建线程。这是一个跨平台的线程库,广泛用于Unix-like系统。

#include <pthread.h>
#include <stdio.h>
 
// 线程执行的函数
void* thread_function(void* arg) {
    printf("Thread is running\n");
    return NULL;
}
 
int main() {
    pthread_t thread_id;
    // 创建线程
    if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
 
    // 等待线程结束
    pthread_join(thread_id, NULL);
    return 0;
}

线程池

设计思路

  • 任务队列:生产者消费者队列
  • 初始化线程固定数量线程
  • 阻塞等待:任务队列为时阻塞
  • 任务加入:对队列加,添加任务
  • 条件变量:唤醒阻塞线程处理任务

线程数量确定因素

  • CPU密集型应用:线程池大小设为CPU核心数量+1
  • IO密集型应用:线程池大小设为2CPU核心数量+1
  • 并行度和响应性需求:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1* CPU数目

sleep和wait的区别?⭐⭐⭐

  • 所属类别:sleep是Thread类的方法,而wait是Object类的方法。
  • 锁的释放:在调用wait时,线程会释放它持有的锁,进入等待状态,并等待其他线程通过notify或notifyAll来唤醒它。而sleep方法不会释放锁,线程会保持对锁的持有。
  • 唤醒方式:调用wait的线程必须依赖其他线程的notify或notifyAll来唤醒它,而sleep方法可以设定一个固定的时间,时间到后线程会自动唤醒。
  • 使用场景:wait通常用于线程间的同步和协作,例如等待其他线程的信号或共享资源的通知。sleep适用于线程的暂时休眠,例如实现定时任务或控制线程执行间隔。

区别

线程与进程的区别

  • 共享资源:线程间共享进程的内存空间,通信更方便,可以直接读写共享内存,而进程间通信需要特定机制,如管道,消息队列和信号量等。
  • 资源消耗:进程的创建和销毁比线程的开销更大,线程的上下文切换更小,只需要保存和恢复线程的上下文,而不是整个进程的状态。
  • 相互依赖:进程是相对独立的,一个进程的崩溃不会影响其他进程,而线程是相互依赖的,一个线程的崩溃会导致整个进程的崩溃。

线程与进程使用场景区别

线程:

  • 任务间需共享数据和资源、频繁通信
  • 轻量级任务、更快切换和调度
  • 实时性要求、更快响应事件和处理任务

进程

  • 独立地址空间和系统资源、数据隔离
  • 更高的安全性和稳定性
  • 并行计算要求,充分利用多核优势

进程、线程优缺点

多进程

  • 优点:独立性、安全性、可拓展性(更容易在多个机器上部署,分布式计算)
  • 缺点:开销大、通信复杂

多线程

  • 优点:轻量级、资源共享、实时响应
  • 缺点:安全问题(数据竞争)、内存占用(独立的栈空间和线程数据结构)、上下文切换开销

一个进程可以创建多少线程,和什么有关⭐

一个进程可以创建的线程数由可用虚拟空间线程的栈的大小共同决定,只要虚拟空间足够,那么新线程的建立就会成功。

理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上一个进程可以创建2048个线程。