一、进程线程的核心区别

对比维度

进程

线程

资源独立性

拥有独立的内存空间、文件描述符等系统资源

共享所属进程的内存空间和资源;

仅拥有独立的栈、程序计数器等;

创建/切换开销

开销大(涉及资源分配与回收)

开销小(仅需要保存少量上下文信息)

通信方式

需通过进程间通信(IPC)机制

(如有无名管道、信号、消息队列等)

可直接通过共享内存通信(需同步机制避免冲突)(互斥锁、条件变量、无名信号量)

健壮性

一个进程崩溃通常不影响其他进程

一个线程崩溃可能导致整个进程崩溃

使用场景

任务间需要严格隔离(如浏览器标签页);

充分利用多核CPU处理CPU密集型任务(如大规模计算);

长时间运行的独立任务(如后台备份、视频转码);

运行不稳定或第三方代码(避免崩溃影响整体);

任务间需要频繁共享数据(如Web服务器处理多请求);

轻量级并发,追求低开销(如GUI程序后台加载数据);

IO密集型任务(如网络请求、文件读写,可利用等待时间切换);

短期、写作紧密的子任务(如数据分片处理后汇总);

二、fork()函数的使用方法以及设计目的

使用:

       函数原型:pid_t fork(void);

       程序使用:pid_t pid = fork();

       功能:用于从当前正在执行的进程(父进程)创建一个新的进程(子进程),新创建的子进程是父进程的一个近乎完全相同的副本。创建过程利用了写时拷贝技术,即父子进程最初共享这些内存页,只有当其中一个进程尝试修改数据时,系统才会真正复制相应的内存页。

fork()函数返回值:

       父进程中返回新创建的子进程的进程 ID (PID),这是一个大于 0 的正整数,父进程可以通过这个 PID 来跟踪和管理它的子进程。

       子进程中返回: 0 ,子进程通过这个返回值知道自己是被 fork() 创建出来的。

       创建失败:在父进程中返回-1。

设计:

       1、区分父子进程:通过不同的返回值,程序可以判断当前是在父进程还是子进程中执行。

       2、给父进程返回子进程 PID,使得父进程能够与子进程通信、等待子进程结束 (wait / waitpid) 或向其发送信号。

       3、无需额外的系统调用。

三、虚拟地址的概念和作用

1、概念

        它是由操作系统为每个程序或进程生成的单独的逻辑地址,不是物理内存上的真实位置,它通过内存管理单元(MMU)映射到物理内存地址。

2、作用

        1)内存隔离:每个进程只能访问自己的虚拟地址空间,无法直接访问其他进程的内存。

        2)文件保护:操作系统可以在虚拟地址到物理地址的映射过程中设置权限(如只读、可读写、可执行等)。

四、写时拷贝技术与计数技术

1、写时拷贝技术:

       当多个实体或进程需要共享同一份数据时,系统并不会创建数据的多个独立副本,而是指向同一份共享数据。只有当某个实体尝试修改数据时,系统才会真正执行复制操作。

       实际应用:创建子进程(fork()函数)

2、计数技术:

       它为一个共享资源关联一个计数器,每当有一个新的实体(如指针)引用这个资源时,计数器加1;当一个实体不再引用该资源时(如指针销毁),计数器减1。当计数器的值降为0时,表示没有任何实体引用该资源,系统可安全释放该资源。

       实际应用:在程序中统计有多少个对象共享数据块。

       缺点:

              1)循环引用问题:如果两个或多个对象相互持有对方的引用(形成环),它们的引用计数永远不会降到 0,导致内存泄漏。

              2)内存占用:计数器通常需要一块独立的内存空间。

五、僵尸态进程存在的意义和回收方法

僵尸态存在的主要原因是为了保证子进程的退出状态和PID等信息能够被父进程正确获取和处理。

回收方法:

        1)父进程主动调用wait()或waitpid()函数

        2)父进程注册SIGCHLD信号处理

             当子进程结束时,内核会向其父进程发送 SIGCHLD 信号。父进程可以预先注册一个信号处理函数,在收到 SIGCHLD 信号时,自动调用 wait() 或 waitpid() 来回收僵尸子进程。

        3)直接结束父进程

             此时的子进程会变成孤儿进程被init进程收养,并负责调用wait()来回收它。

六、线程间的资源共享性

共享

内存地址

所有线程都可以访问进程的整个虚拟内存空间。

文件描述符表

进程打开的文件、管道、套接字等都会在进程的文件描述符表中注册,所有线程都可以使用这些相同的文件描述符进行 I/O 操作。

信号处理器

信号处理器是在进程级别设置的,所有线程共享同一套信号处理函数。

工作目录

进程有一个当前工作目录,所有线程共享该工作目录,如果一个线程改变了工作目录,这个改变对其他所有线程都是可见的。

环境变量

进程启动时继承的环境变量列表,对所有线程都是共享的。

进程ID

所有线程都属于同一个进程,因此它们拥有相同的进程ID。

共享库

进程加载的动态链接库(.so 文件)被所有线程共享。

独有

线程ID

每一个线程都有其独立的线程ID。

寄存器集合

当操作系统在不同的线程之间切换时,需保存和恢复寄存器的值。

线程本地存储

当使用 _thread关键字或pthread_key_create等机制创建的变量,每个线程都有自己独立的副本,互不影响。

错误号变量errno

每个线程拥有自己独立的errno值。

信号掩码

虽然信号处理器是共享的,但每个线程拥有自己独立的信号掩码,这样可以允许一个线程阻塞某些信号,其他线程可以正常接收。

栈(stack)

每个进程都拥有独立的栈空间用于存放局部变量、函数参数、返回地址等。

调度优先级和策略

虽然可以通过线程属性设置调度策略和优先级,但每个线程的调度是独立的。

取消状态和类型

控制线程如何响应取消请求的设置是线程特定的。

浮点运算环境

某些体系结构下,浮点运算相关的控制和状态寄存器也可能为每个线程维护一份。

七、进程间的通信以及特点

1、无名管道:

       1)无名管道只能用于亲缘进程间的通信;

       2)数据在无名管道中只能单向传输,符合半双工通信方式;

       3)无名管道的内核缓冲区大小默认为64kB;

       4)无名管道不能使用lseek函数,lseek函数用于随机访问文件位置,调用会出错返回-1;

       读写特点:

       1)读端存在时:写管道满则阻塞,有4kB空间时唤醒;

       2)读端不存在时:写管道会破裂,会触发SIGPIPE信号;

       3)写端存在时:读管道读取数据,没有数据的时候阻塞等待;

       4)写端不存在时:读管道读取数据,没有数据的时候立即返回0(非阻塞)。

创建无名管道函数:

       函数原型:int pipe(int pipefd[2]);

              创建单向通信通道(半双工通信方式);

              数据只能从读端pipefd[0]读取,从写端pipefd[1]写入,方向不可逆;

              数据并非是在进程间传递,而是先写入内核缓冲区,再由读进程从内核缓冲区读取;

       pipefd:用于存放读写两端的文件描述符的数组首地址。

       创建成功函数返回0;

       创建失败函数返回-1,并重置错误码。

2、有名管道

       1)数据在有名管道中只能单向传输,创建两个管道即可双向通信;

       2)有名管道的内核缓冲区大小默认为64kB;

       3)有名管道不能使用lseek函数,lseek函数用于随机访问文件位置,调用会出错返回-1;

       4)有名管道可用于亲缘进程和非亲缘进程间的通信;

       读写特点:

       1)读端打开时:若管道已满,写操作会阻塞直到有空间;

       2)读端打开时:若写操作在阻塞期间读端关闭,会产生SIGPIPE信号(管道破裂);

       3)读端未打开时:写进程会在open()调用时阻塞,直到有读进程打开该管道;

       4)读端未打开时:若写进程以非阻塞方式打开管道且无读端,open()会返回错误;

       5)写端打开时:读取数据时,有多少读取多少;

       6)写端打开时:若管道为空,读操作会阻塞等待数据写入;

       7)写端未打开时:读进程在open()调用会阻塞,直到有写进程打开该管道;

       8)写端未打开时:若写端曾经打开但已关闭,读操作会读完剩下的数据并返回0(无阻塞);

       9)若读进程以非阻塞方式打开管道且无写端,open()会返回成功。

创建有名管道函数:

       函数原型:int mkfifo(const char *pathname, mode_t mode);

              pathname:文件的名字(默认当前路径)或路径,若管道文件存在会报错;

              mode:文件权限(0666,0664等);

       创建成功函数返回0;

       创建失败函数返回-1,并重置错误码。

删除文件系统中文件名链接的函数:

       函数原型:int unlink(const char *pathname);

              pathname:文件名或文件地址;

       删除成功返回0;

       删除失败返回-1,并重置错误码。

unlink()函数:系统调用函数,用于删除一个文件名及其对应的目录项。如果该文件名是链接到文件数据块的最后一个硬链接,并且没有进程打开该文件,那么文件的实际数据将被释放,磁盘空间会被回收。

4、信号

       它是一种异步通信机制,本质上是软中断,当一个进程接收到信号时会暂停当前执行,转而去处理信号,处理完成后再继续执行。

       特点:

              1)异步性:信号的发送和接收是异步的,不会阻塞发送方;

              2)简单性:信号通常只包含一个整数编号,携带信息量有限;

              3)强制性:某些特殊信号如SIGKILL、SIGSTOP无法被捕捉和忽略;

              4)轻量级:信号处理开销小,适合简单的通知机制。

信号处理方式:

       忽略:对信号不采取任何处理;

       捕获:暂停当前代码的执行,跳转到注册的信号处理函数,执行完后返回原位置;

       默认:执行系统预定的默认动作(通常是终止进程)。

应用场景:

       进程终止控制;

       进程间简单通知;

       定时器实现;

       进程同步。

5、消息队列

       它是存放在内核中的消息链表,由消息队列标识符标识,它允许进程间通过发送和接收消息进行通信,是一种同步/异步的通信方式。

特点:

       数据结构化:可以传输指定类型的数据;

       持久性:消息队列存在于内核中,进程结束后队列仍然存在;

       类型化:支持不同类型的消息,可以按类型选择性接收;

       可靠性:消息不会丢失,直到被接受或队列被删除;

       FIFO(队列)原则:相同类型的消息按先入先出顺序处理。

八、守护进程的功能及其创建方法

功能:

后台运行

脱离终端控制,在后台持续运行。

系统服务

提供网络服务(如Web服务器、数据库等)、定时任务、日志记录等。

资源管理

管理硬件设备、文件系统、网络连接等系统资源。

事件响应

监听系统事件并做出响应(如U盘插入、网络请求等)。

创建守护进程的步骤:
1、创建子进程并退出父进程:
pid_t pid = fork();
if (pid < 0) {
    exit(EXIT_FAILURE); // fork失败
}
if (pid > 0) {
    exit(EXIT_SUCCESS); // 父进程退出
}
	作用:使子进程脱离父进程控制,成为后台进程,并避免子进程成为孤儿进程(父进程退出后,子进程由 init 进程接管)。
2、在子进程中创建新会话:
pid_t sid = setsid();
if (sid < 0) {
    exit(EXIT_FAILURE);
}
	作用:使子进程成为新会话组长,脱离原控制终端的关联,确保进程在后台独立运行。
3、改变当前工作目录:
if ((chdir("/")) < 0) {
    exit(EXIT_FAILURE);
}
	作用:由于子进程(守护进程)继承了父进程的工作目录,当要卸载文件系统时会导致文件卸载失败,所以切换工作目录可以避免这个问题。
4、重设文件权限掩码:
umask(0);
	作用:将进程的文件权限掩码 (umask) 设置为 0。这样守护进程创建的文件和目录将具有预期的权限,而不受原来父进程 umask 值的影响。(修改文件创建的默认权限,避免因继承父进程的 umask 导致文件权限过严或过松。)
5、关闭不必要的文件描述符:
int maxfd = sysconf(_SC_OPEN_MAX);
if (maxfd == -1) maxfd = 1024;
for (int fd = 0; fd < maxfd; fd++) {
    close(fd);
}
	作用:释放进程从父进程继承的文件描述符,避免资源占用,并确保进程不依赖原控制终端的输入输出。
6、重定向标准IO:
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
	作用:避免守护进程与终端有任何关联,防止守护进程尝试读取或写入终端。

九、死锁的产生原因以及如何避免

1、产生原因

       资源不能被多个线程同时使用,一个线程持有互斥锁,在该线程释放锁之前,其他线程无法获取这个锁。

       执行线程已经持有至少一个资源,同时又在等待获取其他正在被别的线程持有的资源。

       资源不能被强制从持有它的线程中夺走,只能由持有该资源的线程自行释放,线程A持有的锁在它主动释放之前,不能被系统或其他线程强制收回。

       循环链路等待情况:多线程情况下,每个线程都在等待下一个线程释放所持有的资源。

2、规避:

       1)每个线程使用完资源后立即释放该资源;

       2)设计协议,使线程在请求新资源前必须释放已持有的资源;

       3)在每次分配资源前进行安全检查,确保不会进入可能导致死锁的状态;

十、线程池的概念和作用

1、概念:

       线程池是包含一组已经创建好的、处于空闲状态的线程集合(队列)。当有任务需要执行时,可以从线程池中获取一个空闲线程来执行任务,任务执行完毕后该线程不会被销毁,而是返回到线程池中继续等待执行下一个任务。

2、作用:

       1)减少资源消耗:通过复用已经创建好的线程,避免了频繁创建和销毁线程带来的系统开销;

       2)提高响应速度:当任务到达时,无需等待新线程的创建可直接从线程池中获取线程执行;

       3)增强线程管理:可以对线程进行统一调度、优化和监控,控制最大并发线程数,防止多线程争夺资源。

十一、线程同步的实现方法以及相关代码

线程同步的实现方法:

       1、互斥锁:确保同一时间只有一个线程可以访问共享资源;

       2、条件变量:让线程在某个条件不满足时挂起,待条件满足后被唤醒;

       3、无名信号量:通过计数器限制并发访问共享资源的数量。

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

//共享资源
int flag = 0; //0:打印A,1:打印B
pthread_mutex_t mutex; //全局互斥锁
pthread_cond_t cond; //全局条件变量

void *print_a(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex); //加锁
        while (flag != 0)
        {
            pthread_cond_wait(&cond, &mutex); //等待当前线程被唤醒
        }
        printf("A\n");
        sleep(1);
        flag = 1;
        pthread_cond_signal(&cond); //唤醒打印B的线程
        pthread_mutex_unlock(&mutex);  //解锁
    }
    return NULL;
}

void *print_b(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex);
        while (flag != 1)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        printf("B\n");
        sleep(1);
        flag = 0;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(int argc,const char *argv[])
{
    pthread_t tid_a, tid_b;
    pthread_mutex_init(&mutex, NULL); //初始化互斥锁
    pthread_cond_init(&cond, NULL); //初始化条件变量
    
    pthread_create(&tid_a, NULL, print_a, NULL); //创建打印A的线程
    pthread_create(&tid_b, NULL, print_b, NULL); //创建打印B的线程
    
    pthread_join(tid_a, NULL); //等待打印A的线程结束
    pthread_join(tid_b, NULL); //等待打印B的线程结束
    
    pthread_mutex_destroy(&mutex); //销毁互斥锁
    pthread_cond_destroy(&cond); //销毁条件变量
    
    return 0;
}

十二、相关函数详解

1、waitpid()

作用:用于等待指定子进程结束,并获取其终止状态。

函数原型:

       pid_t waitpid(pid_t pid, int *status, int options);

       参数:

pid_t pid

指定等待的子进程PID号,其值决定了waitpid的行为;

       pid > 0: 等待进程的 ID 等于子进程PID。

       pid == -1: 等待任意一个子进程,这与 wait() 的行为相同。

       pid == 0: 等待与当前进程具有相同进程组 ID (PGID) 的任意一个子进程。

       pid < -1: 等待任何进程组id与进程pid相等的绝对值的子进程。

int *status

一个指向整型变量的指针,用于存储子进程的退出状态信息,当该参数为NULL时表示不需要获取子进程的退出状态。

退出状态是一个位字段,包含退出码、终止信号等,通常使用宏来解释:

       WIFEXITED(status): 判断子进程是否正常退出(调用 exit 或从 main 返回);如果为真,使用它获取子进程传递给 exit() 的退出码(8位)

       WIFSIGNALED(status): 判断子进程是否被信号终止。如果为真,使用它获取导致子进程终止的信号编号。

       WIFSTOPPED(status): 判断子进程是否被信号暂停(例如 SIGTSTP)。如果为真,使用它获取导致暂停的信号编号。

       WIFCONTINUED(status): 判断子进程是否因收到 SIGCONT 信号而恢复执行(仅当 options 包含 WCONTINUED 时有效)。

int options

标志位,提供额外的控制选项:

       0:默认状态,实现阻塞等待指定的子进程。

       WNOHANG:实现非阻塞等待。如果指定的子进程没有结束,则 waitpid() 不会阻塞,而是立即返回 0。这允许父进程在等待子进程的同时执行其他任务。

       WUNTRACED: 捕获子进程暂停状态,如果子进程被信号停止(stopped),则 waitpid() 会返回该子进程PID,而不是默认忽略停止状态。

       WCONTINUED:捕获子进程恢复运行状态,处于停止状态的子进程被 SIGCONT 信号恢复执行,则 waitpid() 会返回该子进程PID。

函数返回值

成功时:

       返回终止了的子进程PID;

       如果设置了非阻塞等待(WNOHANG)且没有子进程退出,返回0;

失败时:

       返回-1,并设置errno来显示错误原因。

2、wait()函数

作用:

       用于父进程阻塞等待其任意一个子进程终止并获取其终止状态的函数。

函数原型:

       pid_t wait(int *status);

参数:

       status:int类型的指针变量,用于存储子进程终止状态信息,信息包含了子进程是正常退出还是被信号终止,以及具体的退出码或信号编号。

       当其值为NULL时表示父进程不需要读取子进程的具体终止状态。

函数返回值:

       成功时返回终止的子进程的进程PID;

       失败时返回-1,并设置errno来显示错误信息。

3、exit()函数

作用:

       用于正常终止调用它的进程。

函数原型:

       void exit(int status);

参数:

       status:进程的退出状态码,传递给父进程;为0时表示进程成功终止,非0值代表进程异常终止。

       例如:某一行出现"exit();"则该行接下来的代码将不会执行。

4、_exit()函数

作用:

       无条件立即终止调用它的进程。

函数原型:

       void _exit(int satus);

参数:

       status:和exit()函数相同

它与exit()函数的关键区别:

       清理操作:

              exit(): 调用 atexit() 注册的函数;刷新并关闭所有标准 I/O 流的缓冲区。

              _exit(): 不调用 atexit() 注册的函数;不刷新或关闭标准 I/O 流。

       调用类型:

              exit():是标准C库函数;

              _exit():是系统调用。

使用方法:

       在 fork() 之后的子进程中: 这是最常见的使用场景。如果子进程在 fork() 后立即调用 exit(),它可能会刷新和关闭父进程也正在使用的共享文件描述符(因为 fork 后子进程继承了父进程的文件描述符和缓冲区状态)。为了避免这种潜在问题,子进程通常在完成任务后直接调用 _exit()。

5、dup和dup2函数

1)dup()函数

作用:dup(oldfd) 创建一个新的文件描述符,它是 oldfd 的副本。

       新的文件描述符是当前可用的最小的未使用文件描述符号。

       两个文件描述符(oldfd 和新返回的 FD)都指向同一个打开的文件描述(Open File Description)。这意味着它们共享文件偏移量(file offset)和文件状态标志(file status flags)。对其中一个的操作会影响另一个。

函数原型:int dup(int oldfd);

       oldfd:需要被复制的现有文件描述符。

函数返回值:

              成功时返回一个新的文件描述符,它是oldfd的副本;

              失败时返回-1,设置errno来显示错误原因。

2)dup2()函数

作用:dup2(oldfd, newfd) 将 oldfd 复制到 newfd 指定的文件描述符号上。

       如果 newfd 已经打开,dup2() 会先尝试关闭它(除非 oldfd 和 newfd 是同一个文件描述符)。

       与 dup() 一样,oldfd 和 newfd 最终都指向同一个打开的文件描述。

       dup2() 提供了比 dup() 更精确的控制,你可以指定新文件描述符的确切号码。

函数原型:int dup2(int oldfd, int newfd);

       oldfd (参数):现有文件描述符。

       newfd (参数): 新文件描述符的号码。

示例代码片段:

int backup_stdout = dup(STDOUT_FILENO); // 备份原始 stdout
    if (backup_stdout == -1) {
        perror("dup");
        close(fd);
        exit(EXIT_FAILURE);
    }

    if (dup2(fd, STDOUT_FILENO) == -1) { // 将 stdout 重定向到文件
        perror("dup2");
        close(fd);
        close(backup_stdout);
        exit(EXIT_FAILURE);
    }

十三、进程的状态

进程的状态:

运行状态

R

也称之为可运行状态,也就是处于运行状态的进程,正在CPU执行或等待CPU调度。

可中断睡眠状态

S

进程在等待某个事件(IO操作、等待特定信号等)完成,可被信号唤醒。

不可中断睡眠状态

D

进程正在等待关键硬件操作完成,不能被信号中断,确保数据完整性。

停止状态

T

进程被暂停执行,通常是收到了特定信号或者在调试程序时被调试器暂停,可被SIGCONT信号恢复执行。

僵尸状态

Z

进程已终止,但资源未被回收,等待父进程进行回收。

死亡态

X

进程彻底终止,资源全部释放,是一个瞬间态,几乎无法观测。

进程状态之间的转换:

死亡态(X)到运行态(R)

触发:fork()或clone()系统调用。

过程:创建新进程,初始化进程控制块(PCB),分配必要的系统资源。

注意点:X是一个瞬时态,会很快转换为R态。

R到D

触发:执行底层IO操作(如直接访问磁盘文件)、等待硬件设备响应、执行关键内核操作。

D到R

底层操作完成,它不会因为信号而被中断唤醒。

R到T

触发:收到SIGSTOP信号、收到SIGTSTP信号(终端停止)、调试器暂停。

T到R

收到SIGCONT信号(继续执行)、调试器发出继续指令。

R到Z

触发:子进程正常退出。

过程:释放进程占用的资源,保留进程ID和退出状态,进程控制块仍存在于系统中,等待父进程调用wait()等系统调用。

Z到X

触发:父进程调用wait()、waitpid()等系统调用函数。

R到R

触发:时间片用完(时间片轮转调度)、被更高优先级进程抢占等。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐