【Linux的线程篇章 - 线程、进程、多线程、多进程等知识】

Linux之线程、进程、多线程、多进程等知识

前言:
前篇开始进行了解学习Linux线程基本知识等相关内容,接下来学习关于Linux线程、进程、多线程、多进程等知识储备,深入地了解这个强大的开源操作系统。
/知识点汇总/

1、线程

线程(Thread)是计算机程序中的一个重要组成部分,它是操作系统能够进行运算调度的最小单位。

1.2、线程的定义

1.定义:

线程是程序执行时的最小单元,它是操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位。

2.特性:

a、独立性:线程是独立调度和分派的基本单位,它可以独立运行、被中断和恢复运行。
b、共享性:同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符和信号处理等。
c、并发性:一个进程可以并发多个线程,每条线程并行执行不同的任务。

1.3、线程的结构与组成

1.线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。这使得线程可以独立运行,并与其他线程进行交互。
2.线程的实体包括程序、数据和线程控制块(TCB),其中TCB用于描述线程的动态特性,如线程状态、被保存的现场资源、执行堆栈等。

1.4、线程的类型

1.用户线程:
由程序创建的线程,通常称为前台线程。它们运行在使用者的程序中,当程序的主线程结束时,用户线程不一定结束,仍然可以继续运行,直到完成任务或被手动停止。
2.守护线程:
一种特殊的线程,它在程序运行过程中在后台运行,主要用来为其他线程和应用程序提供服务。当只剩下守护线程时,Java虚拟机(JVM)会自动退出。
3.本地线程(Native Thread):
指使用特定于本机的线程实现的线程。Java运行时与本地线程交互,将Java线程映射到本地线程中。

1.5、线程的状态

线程在其生命周期中,通常会有以下几种状态:
1.就绪状态:
线程具备运行的所有条件,逻辑上可以运行,但正在等待处理机。
2.运行状态:
线程占有处理机正在运行。
3.阻塞状态:
线程在等待一个事件(如某个信号量),逻辑上不可执行。

2、线程同步与安全问题

由于多个线程可能同时访问共享资源,因此需要采取同步措施来保证数据的一致性和安全性。常见的同步机制包括锁(Locks)、条件变量(ConditionVariables)、信号量(Semaphores)等。

2.1、互斥锁(Mutex)

1.定义:

互斥锁是一种用于保证在同一时间只有一个线程可以访问共享资源的机制。

2.工作原理:

当一个线程获得了互斥锁后,其他线程需要等待该线程释放锁才能继续访问共享资源。

3.应用:

适用于需要严格控制资源访问顺序的场景,如文件操作、数据库访问等。

4.C++ 互斥锁样例
在C++中,我们可以使用库中的std::mutex来实现互斥锁。

#include <iostream>  
#include <thread>  
#include <mutex>  
  
std::mutex mtx; // 全局互斥锁  
  
void print_block(int n, char c) {  
    mtx.lock(); // 锁定互斥锁  
    for (int i = 0; i < n; ++i) { std::cout << c; }  
    std::cout << '\n';  
    mtx.unlock(); // 解锁互斥锁  
}  
  
int main() {  
    std::thread threads[10];  
    // 创建10个线程,每个线程打印不同的字符块  
    for (int i = 0; i < 10; ++i)  
        threads[i] = std::thread(print_block, 50, 'A' + i);  
  
    for (auto& th : threads) th.join(); // 等待所有线程完成  

    return 0;  
}

2.2、信号量(Semaphore)

1.定义:

信号量是一种用于控制多个线程对共享资源访问的计数器。与互斥锁不同,信号量允许多个线程同时访问资源,但数量受到信号量值的限制。本质上信号量是一种计数器,用于控制同时访问某个共享资源的线程数量。

2.工作原理:

当计数器大于0时,线程可以访问资源并将计数器减1;当计数器等于0时,线程需要等待其他线程释放资源后才能继续访问。

3.应用:

适用于允许多个线程同时访问资源,但需要限制访问数量的场景,如连接池管理、资源池分配等。

4.C语言样例(使用POSIX信号量):
在POSIX中,信号量通过<semaphore.h>库提供。这个库定义了两种类型的信号量:sem_t(用于进程间通信的未命名信号量)和sem_unlink()(用于删除未命名信号量),以及可能通过其他机制(如文件系统)提供的命名信号量。

#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
#include <semaphore.h>  
#include <unistd.h>  
  
sem_t sem;  
  
void* thread_func(void* arg) {  
    sem_wait(&sem); // 等待信号量  
    // 访问共享资源  
    printf("Thread %ld is accessing the resource\n", (long)arg);  
    sleep(1); // 模拟耗时操作  
    // 释放共享资源  
    sem_post(&sem); // 增加信号量  
    return NULL;  
}  
  
int main() {  
    pthread_t threads[5];  
    sem_init(&sem, 0, 2); // 初始化信号量,最大值为2  
  
    for (long i = 0; i < 5; i++) {  
        pthread_create(&threads[i], NULL, thread_func, (void*)i);  
    }  
  
    for (int i = 0; i < 5; i++) {  
        pthread_join(threads[i], NULL);  
    }  
  
    sem_destroy(&sem); // 销毁信号量  
    return 0;  
}

2.3、条件变量(Condition Variable)

1.定义:

条件变量用于线程之间的通信和协调。
要点:
a、条件变量用于线程间的同步,允许一个或多个线程在某个条件成立时被唤醒。
b、条件变量通常与互斥锁一起使用,以避免在条件等待和通知过程中发生竞态条件。

2.工作原理:

一个线程可以等待某个条件的发生,而另一个线程可以在满足条件时通知等待的线程继续执行。

3.应用:

适用于需要线程间协调或等待某个条件满足后再继续执行的场景,如生产者-消费者问题、等待队列等。

4.C语言样例(使用POSIX线程库):

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  
  
std::mutex mtx;  
std::condition_variable cv;  
bool ready = false;  
  
void print_id(int id) {  
    std::unique_lock<std::mutex> lck(mtx);  
    while (!ready) cv.wait(lck); // 等待ready变为true  
    std::cout << "Thread " << id << '\n';  
}  
  
void go() {  
    std::unique_lock<std::mutex> lck(mtx);  
    ready = true;  
    cv.notify_all(); // 通知所有等待的线程  
}  
  
int main() {  
    std::thread threads[10];  
    // 创建10个线程  
    for (int i = 0; i < 10; ++i)  
        threads[i] = std::thread(print_id, i);  
  
    std::cout << "10 threads ready to race...\n";  
    go(); // 通知线程开始执行  
  
    // 等待所有线程完成  
    for (auto& th : threads) th.join();  
  
    return 0;  
}

2.4、读写锁(Read-Write Lock)

1.定义:

读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

2.工作原理:

当没有线程写入资源时,多个线程可以同时读取资源;当有线程写入资源时,其他线程(包括读取线程和写入线程)都需要等待。

3.应用:

适用于读操作远多于写操作的场景,如缓存系统、数据库查询等。

4.C++ 读写锁样例
在C++中,<shared_mutex>库(从C++17开始引入)提供了std::shared_mutex,这是一个读写锁的实现。但请注意,如果你的编译器或标准库不支持C++17,你可能需要寻找其他实现或使用第三方库。

#include <iostream>  
#include <thread>  
#include <shared_mutex>  
#include <vector>  
  
std::shared_mutex rw_mtx; // 读写锁  
int data = 0; // 被保护的共享数据  
  
void reader(int id) {  
    std::shared_lock<std::shared_mutex> lock(rw_mtx); // 获取共享锁  
    std::cout << "Reader " << id << " reads " << data << '\n';  
}  
  
void writer(int id, int value) {  
    std::unique_lock<std::shared_mutex> lock(rw_mtx); // 获取独占锁  
    data = value;  
    std::cout << "Writer " << id << " writes " << data << '\n';  
}  
  
int main() {  
    std::vector<std::thread> threads;  
  
    // 创建写线程  
    for (int i = 0; i < 5; ++i)  
        threads.emplace_back(writer, i, i*10);  
  
    // 创建读线程  
    for (int i = 0; i < 5; ++i)  
        threads.emplace_back(reader, i);  
  
    for (auto& th : threads) th.join(); // 等待所有线程完成  
  
    return 0;  
}

2.5、原子操作

1.定义:

原子操作,指的是在执行过程中不会被任何其他任务或事件打断的操作,也就是说,它是不可分割的最小执行单位。在编程和操作系统中,原子操作对于实现同步和避免竞态条件至关重要。
即:原子操作是一种不可中断的操作,要么全部执行成功,要么全部不执行。

2.工作原理:

原子操作通过硬件或操作系统的支持,保证在多个线程同时访问共享资源时的数据一致性。

3.应用:

适用于需要保证数据一致性的场景,如计数器更新、状态标志修改等。

4.(使用信号量实现互斥访问)示例:
在这个例子中,我们将使用POSIX信号量来实现对共享资源的互斥访问。信号量的操作(如sem_wait()和sem_post())通常是原子操作,确保了在并发环境下的安全执行。

#include <stdio.h>  
#include <pthread.h>  
#include <semaphore.h>  
#include <unistd.h>  
  
sem_t sem;  
  
void* thread_function(void* arg) {  
    // 等待信号量,如果信号量的值为0,则阻塞当前线程  
    sem_wait(&sem);  
  
    // 执行对共享资源的访问  
    printf("Accessing shared resource\n");  
    sleep(1); // 模拟资源访问耗时  
  
    // 释放信号量,允许其他线程访问共享资源  
    sem_post(&sem);  
  
    return NULL;  
}  
  
int main() {  
    pthread_t t1, t2;  
  
    // 初始化信号量,初始值为1,表示允许一个线程同时访问共享资源  
    sem_init(&sem, 0, 1);  
  
    // 创建两个线程  
    pthread_create(&t1, NULL, thread_function, NULL);  
    pthread_create(&t2, NULL, thread_function, NULL);  
  
    // 等待线程结束  
    pthread_join(t1, NULL);  
    pthread_join(t2, NULL);  
  
    // 销毁信号量  
    sem_destroy(&sem);  
  
    return 0;  
}

在这个例子中,sem_wait()和sem_post()操作是原子操作。sem_wait()在信号量的值大于0时将其减1并立即返回,允许线程继续执行;如果信号量的值为0,则sem_wait()会阻塞调用线程,直到信号量的值变为非零。sem_post()则将信号量的值加1,并唤醒可能因sem_wait()而阻塞的线程。
需要注意的是,虽然sem_wait()和sem_post()是原子操作,但整个对共享资源的访问过程(包括等待信号量、访问资源和释放信号量)本身并不是一个单一的原子操作。为了确保线程安全,我们还需要结合信号量来确保对共享资源的互斥访问。

2.6、屏障(Barrier)

1.定义:
屏障用于线程之间的同步,它可以让一组线程在某个点上等待,直到所有线程都到达这个点后再继续执行。

2.工作原理:
当线程到达屏障点时,它们会被阻塞,直到所有线程都到达屏障点,然后它们会同时被释放并继续执行。

3.应用:
适用于需要等待所有线程都完成某个阶段后再继续执行的场景,如并行计算中的阶段同步。

2.7、同步方法和同步块

1.定义:
在编程语言中,如Java,可以通过synchronized关键字来实现同步方法和同步块。
2.工作原理:
同步方法或同步块会获取对象的锁,确保在同一时间内只有一个线程可以执行该方法或块内的代码。
3.应用:
适用于需要保护特定方法或代码块不被多个线程同时执行的场景。

3、线程的操作

线程的相关操作通常涉及到线程的创建、同步、互斥、通信、终止以及获取线程信息等
在Linux系统中,线程的创建、属性设置、回收、退出、取消、终止、等待、分离等操作通常是通过POSIX线程(pthread)库来实现的。

3.1、线程的创建

在Linux系统中,线程的创建通常通过pthread_create函数实现。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

thread:用于存储新线程的标识符。
attr:指定线程的属性,如果传递NULL,则使用默认属性。
start_routine:线程启动后要执行的函数。
arg:传递给线程启动函数的参数。
返回值:成功时,pthread_create返回0;失败时,返回错误码。

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
void* thread_function(void* arg) {  
    printf("Hello from thread!\n");  
    return NULL;  
}  
  
int main() {  
    pthread_t thread_id;  
    int ret = pthread_create(&thread_id, NULL, thread_function, NULL);  
    if (ret != 0) {  
        perror("pthread_create failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待线程结束  
    pthread_join(thread_id, NULL);  
  
    printf("Thread has finished execution.\n");  
  
    return 0;  
}

3.2、线程的终止

线程可以通过多种方式终止执行:
1.从线程函数正常返回:

当线程函数执行完成并返回时,线程终止。

2.调用pthread_exit:

该函数允许线程显式地终止执行并返回一个指向任意类型值的指针,但需要注意的是,这个指针指向的内存单元必须是全局的或者是用malloc等函数分配的,不能在线程函数的栈上分配。

void* thread_function(void* arg) {  
    printf("Thread is about to exit.\n");  
    pthread_exit(NULL); // 显式退出线程  
    // 注意:下面的代码不会被执行  
    printf("This line will not be printed.\n");  
}

3.调用pthread_cancel:

该函数可以用来请求取消同一进程中的其他线程。但是,线程是否响应取消请求取决于其取消状态以及取消点(即线程检查取消请求的点)。

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
  
void* thread_function(void* arg) {  
    while (1) {  
        sleep(1); // 模拟长时间运行  
        printf("Thread is running...\n");  
    }  
    return NULL;  
}  
  
int main() {  
    pthread_t thread_id;  
    pthread_create(&thread_id, NULL, thread_function, NULL);  
  
    sleep(5); // 等待一段时间  
    pthread_cancel(thread_id); // 请求取消线程  
  
    // 注意:默认情况下,线程可能不会立即退出,因为它可能不在取消点  
  
    pthread_join(thread_id, NULL); // 等待线程真正退出  
  
    printf("Thread has been canceled.\n");  
  
    return 0;  
}

3.3、线程的等待

线程退出后,其资源并不会立即被释放,而是需要由其他线程(通常是主线程)通过调用pthread_join函数来等待并回收。

int pthread_join(pthread_t thread, void **retval);

thread:指定要等待的线程标识符。
retval:如果不为NULL,则用来存储被等待线程的返回值。
返回值:成功时,pthread_join返回0;失败时,返回错误码。

3.4、线程的属性设置

线程的属性可以通过pthread_attr_t结构体进行设置。
主要步骤包括:

1.定义并初始化线程属性变量。
2.调用相应的属性设置函数(如pthread_attr_setdetachstate等)来设置属性。
3.在创建线程时,将设置好的属性传递给pthread_create函数。
4.在不再需要时,销毁线程属性变量。

#include <pthread.h>  
#include <stdio.h>  
  
int main() {  
    pthread_attr_t attr;  
    pthread_attr_init(&attr);  
    // 可以设置其他属性,如pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);  
  
    // 注意:这里仅演示了如何初始化属性对象,并没有创建线程  
  
    pthread_attr_destroy(&attr);  
  
    return 0;  
}

3.5、线程的分离

线程分离(Detach)是指线程在结束时,操作系统会自动回收其占用的资源,而不需要其他线程(如主线程)显式地等待该线程的结束或调用如pthread_join之类的函数来回收资源。这种机制提高了程序的并发性和可维护性,因为主线程或其他线程不需要关心子线程的状态和清理工作。

在POSIX线程(pthread)库中,pthread_detach函数用于将线程设置为分离状态

#include <pthread.h>  
int pthread_detach(pthread_t thread);

thread:要设置为分离状态的线程标识符。
返回值:成功时,pthread_detach函数返回0;失败时,返回错误码。

线程分离通常用于以下几种情况:

1.不关心线程返回值:
当主线程或其他线程不关心某个子线程的返回值时,可以将该子线程设置为分离状态,以避免调用pthread_join所带来的额外开销和复杂性。
2.提高并发性:
通过允许线程自动回收资源,可以减少线程之间的同步和等待,从而提高程序的并发性。
3.避免僵尸线程:
在默认情况下,如果线程没有被其他线程join,它会变成一个僵尸线程(zombie thread),占用系统资源但不执行任何操作。通过设置线程为分离状态,可以避免这种情况的发生。

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
void* thread_function(void* arg) {  
    // 模拟线程执行任务  
    printf("线程正在运行...\n");  
    // 线程执行完毕后自动退出,无需其他线程join  
    pthread_exit(NULL);  
}  
  
int main() {  
    pthread_t thread_id;  
    int ret = pthread_create(&thread_id, NULL, thread_function, NULL);  
    if (ret != 0) {  
        perror("pthread_create failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 将线程设置为分离状态  
    if (pthread_detach(thread_id) != 0) {  
        perror("pthread_detach failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 主线程继续执行其他任务,无需等待线程结束  
    printf("主线程继续执行...\n");  
  
    // 模拟主线程的其他任务  
    sleep(2); // 休眠2秒以观察输出  
  
    printf("主线程结束\n");  
  
    return 0;  
}

3.6、线程的资源回收

线程在退出时,应当及时释放其所占用的资源,包括打开的文件、动态分配的内存等。对于可结合的线程(默认属性),必须显式地调用pthread_join来回收其资源;而对于分离的线程(通过设置属性为PTHREAD_CREATE_DETACHED),其资源则会在线程退出时由系统自动回收。

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
void* thread_function(void* arg) {  
    printf("Hello from thread!\n");  
    return NULL;  
}  
  
int main() {  
    pthread_t thread_id;  
    int ret = pthread_create(&thread_id, NULL, thread_function, NULL);  
    if (ret != 0) {  
        perror("pthread_create failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待线程结束  
    pthread_join(thread_id, NULL);  
  
    printf("Thread has finished execution.\n");  
  
    return 0;  
}

4、进程

4.1、进程的定义

Linux进程是操作系统进行资源分配和调度的一个独立单元,它是程序的一个动态执行过程,包含了程序代码、数据和系统分配给该程序的各种资源(如内存、文件描述符等)。每个Linux进程都有自己唯一的进程ID(PID),并且操作系统通过PID来管理和识别进程。
进程ID(PID):每个进程都有一个唯一的标识符,即PID。PID是一个非负整数,通常从1开始。

4.2、进程的特征

1.动态性:

进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。

2.并发性:

指多个进程实体同时存于内存中,能在一段时间内同时运行。并发性是进程的重要特征,同时也是操作系统的重要特征。

3.独立性:

进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单元。

4.异步性:

由于进程的相互制约,使得进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。

5.结构性:

每个进程都配置一个PCB(进程控制块)对其进行描述,进程实体由程序段、数据段和PCB三部分组成。它包含了进程的各种信息,如PID、状态、优先级、程序计数器、CPU寄存器、内存管理信息等。

4.3、进程的组成与描述

1.程序段:
能被进程调度到CPU执行的程序代码段(指令序列)。
2.数据段:
进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。
3.进程的描述:PCB(进程控制块):

PCB(Process Control Block,进程控制块)是操作系统用于描述和管理进程的数据结构。 在Linux系统中,PCB的具体实现是task_struct结构体。每个进程在创建时都会分配一个PCB,并在进程运行的全过程中伴随其左右,直到进程被撤销。PCB中包含了进程的各种信息,用于操作系统对进程进行调度、管理和控制。

以下是PCB中可能包含的一些关键信息:

属性描述
进程标识符(PID)唯一标识一个进程
进程状态描述进程当前所处的状态,如就绪、运行、阻塞等
程序计数器指向当前进程执行的指令地址
CPU寄存器保存CPU的当前状态,以便在进程切换时能够恢复
内存管理信息包括进程的内存分配情况、页表等
文件描述符表记录进程打开的文件信息
信号量用于进程间的同步和通信
优先级决定进程被调度的优先级
父进程和子进程信息用于维护进程之间的父子关系

PCB是操作系统感知进程存在的唯一标志,通过PCB,操作系统能够实现对进程的有效管理和控制。在Linux系统中,task_struct结构体是PCB的具体实现,它包含了进程所需的所有信息,是进程管理和调度的核心数据结构。

4.4、进程的状态

在Linux系统中,进程的状态反映了进程的执行情况和系统对进程的管理方式。
进程在其生命周期中会经历多种状态,主要包括:
1.就绪状态(Ready):

程已经准备好执行,但尚未被处理器(CPU)调度执行。此时,进程已经具备了所有必要的资源(除了CPU),只需要等待CPU的调度即可开始执行。

2.运行状态(Running):

进程正在被CPU执行。在Linux中,进程并不是一直占用CPU直到执行完毕,而是采用时间片轮转的方式,每个进程分配一个时间片来执行。时间片结束后,进程会被切换出去,让其他进程有机会执行。

3.阻塞状态(Blocked):

进程因为等待某些事件(如I/O操作完成、信号量等)而无法继续执行。在阻塞状态下,进程不会占用CPU资源,直到等待的事件发生并被操作系统唤醒。

4.睡眠状态(Sleeping,也称为等待状态Waiting):

进程因为等待某些条件(如I/O操作完成、信号等)而被阻塞,但与阻塞状态不同的是,睡眠状态更侧重于描述进程在等待过程中可以被中断(可中断睡眠状态,如S状态)或不可被中断(不可中断睡眠状态,如D状态)。

5.暂停状态(Paused):

进程被暂停执行,但可以通过某种方式(如发送SIGCONT信号)自行恢复执行。暂停状态通常是由用户或系统通过发送SIGSTOP信号给进程来实现的。

6.挂起状态(Suspended):

进程被操作系统挂起执行,资源被释放,进程状态被保存到磁盘上。挂起状态可以分为内存挂起状态和磁盘挂起状态两种。挂起状态通常用于在资源不足时暂时淘汰某些进程,以便在资源充足时重新调入执行。

7.终止状态(Terminated):

进程执行完毕或因为某些原因被终止。在终止状态下,进程的所有资源(包括PCB)都会被释放,进程不再存在于系统中。

8.Disk Sleep(磁盘休眠状态,也称为不可中断睡眠):

当进程正在进行磁盘I/O操作,且由于硬件交互的原因,该进程不能被其他进程或中断打断时,进程将处于Disk Sleep状态。
状态解释:
进程正在与硬件交互,且交互过程不允许被其他进程或中断打断。这通常发生在磁盘I/O操作中。

9.Zombie(僵尸状态):

程已经结束执行,但其进程描述符(task_struct)仍然存在于系统中,直到父进程通过wait()系统调用来回收它。僵尸进程会占用系统资源,因此应该避免产生过多的僵尸进程。

10.Tracing Stop(跟踪暂停状态):

使用调试器(如gdb)调试进程时,在断点处进程会暂停,此时进程处于Tracing Stop状态。
状态解释:
进程被调试器暂停,等待调试器的下一个信号来执行具体的操作。这是一种特殊的暂停状态,用于调试或监控进程。

4.4.1、两种特殊状态

1.僵尸进程(Zombie Process)

定义:

僵尸进程是指那些已经完成了执行(即已经调用了exit()或被其他系统调用结束),但是其父进程尚未通过调用wait()或waitpid()来回收其子进程的终止状态,从而导致这些子进程的进程描述符仍然保留在系统中的进程。这些进程已经失去了几乎所有的内存资源(除了进程描述符),但它们仍然存在于进程表中,占用系统资源(主要是PID)。

危害:

占用PID资源。在Linux系统中,PID是有限的,如果系统中产生了大量的僵尸进程,可能会导致无法创建新的进程,因为PID资源耗尽。
占用系统资源。虽然僵尸进程本身不占用太多资源,但它们仍然保留在进程表中,需要系统维护这些进程的信息。

处理方式:

1.确保回收:父进程应该通过调用wait()或waitpid()来回收子进程的终止状态。这是防止僵尸进程产生的根本方法。
2.重新父进程:在父进程先于子进程结束的情况下,子进程会被init进程(PID为1的进程)收养,init进程会周期性地调用wait()来回收这些子进程的终止状态,从而避免僵尸进程的产生。

2.孤儿进程(Orphan Process)
定义:

孤儿进程是指那些父进程已经终止或被杀死,但其子进程仍然在运行,这些子进程将被init进程(PID为1的进程)收养的进程。

特点:

孤儿进程并不是一种有害的进程状态,它们只是被init进程收养,继续执行其任务。
孤儿进程的结束状态会被init进程回收,因此不会产生僵尸进程的问题。

处理方式:

实际上,对于孤儿进程,系统已经通过init进程进行了自动处理,因此通常不需要用户或程序员进行额外的处理。
但是,在设计程序时,应该考虑到可能的孤儿进程情况,确保这些进程能够正常地执行完其任务,并在完成后正确地结束。

4.5、进程的管理与调度

**进程管理:**操作系统通过PCB对进程进行管理,包括进程的创建、撤销、状态转换等。
**进程调度:**操作系统根据一定的算法(如优先级算法)从就绪队列中选择一个进程,让它占用CPU执行。进程调度的目的是合理分配计算机资源,提高系统效率。

Linux提供了多种工具来管理和监控进程,包括但不限于:
1.ps命令:

用于显示当前系统中的进程状态。通过不同的选项组合,可以获取进程的详细信息,如PID、父进程ID(PPID)、CPU和内存占用率、启动时间等。

2.top命令:

实时显示系统中各个进程的资源占用情况,包括CPU、内存等。它还可以按照不同的指标对进程进行排序。

3.kill命令:

用于发送信号给进程,通常用于终止进程。通过指定PID和信号类型,可以控制进程的行为。

4.pstree命令:

以树状图的形式显示进程之间的父子关系,有助于理解进程的层次结构。

5.nice和renice命令:

用于调整进程的优先级。nice命令在启动进程时设置其优先级,而renice命令则用于修改已经运行的进程的优先级。

5、进程间通信(IPC)

在Linux系统中,进程间通信(InterProcess Communication, IPC)是指不同进程之间或同一进程的不同线程之间传递信息或数据的过程。
由于Linux是一个多任务的操作系统,进程间通信对于实现资源共享、任务协作和数据交换至关重要。Linux提供了多种进程间通信的方式,包括管道(Pipe)、消息队列(Message Queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)等。

5.1、管道(Pipe)

管道是Linux中最古老的IPC机制之一,它允许一个进程(称为写进程)将数据写入到管道的一端,而另一个进程(称为读进程)可以从管道的另一端读取数据。管道是单向的,即数据只能从一个方向流动。管道主要分为匿名管道和命名管道(FIFO,First In First Out)。匿名管道主要用于父子进程或具有共同祖先的进程间的通信,而命名管道则允许不相关的进程进行通信。

5.2、消息队列(Message Queue)

消息队列是一种消息的链接列表,存储在内核中并由消息队列标识符标识。它允许一个或多个进程向它写入或从中读取消息。与管道相比,消息队列的优点在于它能够独立于发送和接收进程而存在,并且支持多个读者和多个写者。消息队列也支持消息的优先级,允许高优先级的消息优先被读取。

5.3、共享内存(Shared Memory)

共享内存是多个进程共享的一块内存区域。这是最快的一种IPC方式,因为进程可以直接读写内存,而不需要数据的复制。 但是,由于多个进程可以同时访问同一块内存,因此需要使用某种同步机制(如信号量)来避免冲突。

5.4、信号量(Semaphore)

**信号量是一种计数器,用于控制对共享资源的访问。**它主要用于解决多个进程或线程间的同步问题,以避免竞态条件。信号量可以用于控制对共享内存的访问,也可以用于实现其他类型的同步原语,如互斥锁和条件变量。

5.5、套接字(Socket)

套接字是一种更为通用的进程间通信机制,它不仅支持同一台机器上的进程间通信,还支持不同机器上的进程间通信(即网络通信)。套接字提供了一种基于文件描述符的通信方式,它允许应用程序打开、读写和关闭网络连接,就像操作文件一样。套接字支持多种通信协议,如TCP、UDP等。

6、进程的操作

在Linux系统中,进程的涉及创建、终止、等待、状态查询、通信以及挂起和恢复等操作。

6.1、创建进程

系统调用函数:
在UNIX/Linux系统中,可以使用fork()系统调用函数来创建一个新的进程。
fork()函数会创建一个与当前进程几乎完全相同的子进程,但子进程会获得一个唯一的进程标识符(PID)。
其他方法:
除了fork(),还可以使用vfork()(在某些情况下更高效,但限制更多)和exec()族函数(用于替换当前进程的映像,从而执行另一个程序)来间接创建新进程或改变进程的执行内容。

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/wait.h>  
  
int main() {  
    pid_t pid = fork();  
    if (pid == -1) {  
        // 创建进程失败  
        perror("fork failed");  
        exit(EXIT_FAILURE);  
    } else if (pid == 0) {  
        // 子进程执行的代码  
        printf("This is the child process, PID = %d\n", getpid());  
    } else {  
        // 父进程执行的代码  
        printf("This is the parent process, PID = %d, Child PID = %d\n", getpid(), pid);  
        // 等待子进程结束  
        wait(NULL);  
    }  
    return 0;  
}

6.2、终止进程

系统调用函数:
可以使用exit()系统调用函数来终止一个进程。 exit()函数会结束调用它的进程,并返回一个状态码给父进程或操作系统。在UNIX/Linux系统中,还可以使用kill()系统调用函数向一个进程发送终止信号,从而强制终止该进程。
其他方式:
进程还可以因为接收到特定的信号(如SIGINT、SIGTERM等)而终止,或者当主程序执行完毕时自然退出。

// 在子进程中使用exit()终止  
if (pid == 0) {  
    // 子进程执行的代码  
    printf("Exiting child process...\n");  
    exit(EXIT_SUCCESS); // 正常退出子进程  
}
// 假设已知子进程PID为child_pid  
kill(child_pid, SIGTERM); // 向子进程发送SIGTERM信号请求终止

6.3、进程等待

系统调用函数:
可以使用wait()或waitpid()系统调用函数来使一个进程(通常是父进程)等待另一个进程(子进程)的终止。这两个函数可以获取子进程的终止状态,并释放子进程所占用的资源。

// 在父进程中等待子进程结束  
pid_t wpid = wait(NULL); // 阻塞等待子进程结束  
if (wpid == -1) {  
    // 等待出错  
    perror("wait failed");  
} else {  
    printf("Child process %d has exited\n", wpid);  
}

6.4、进程状态查询

命令工具:
可以使用ps命令来查询当前系统正在运行的进程的状态,包括进程的ID、CPU使用情况、内存占用等信息。

ps -ef//该命令会列出系统上当前所有的进程及其详细信息。

6.5、进程通信

IPC技术:
可以使用多种进程间通信(IPC)技术来实现进程之间的数据交换和通信,包括管道(pipe)、消息队列(message queue)、共享内存(shared memory)和套接字(socket)等。

// 父进程创建管道,并分别向管道写和读数据(实际应用中通常由两个进程分别进行)  
int fd[2];  
pipe(fd);  
  
// 假设有子进程通过fork()创建  
if (pid == 0) {  
    // 子进程写数据到管道  
    close(fd[0]); // 关闭读端  
    write(fd[1], "Hello, parent!", 14);  
    close(fd[1]);  
} else {  
    // 父进程从管道读数据  
    close(fd[1]); // 关闭写端  
    char buf[100];  
    read(fd[0], buf, sizeof(buf));  
    printf("Received from child: %s\n", buf);  
    close(fd[0]);  
}

6.6、进程挂起和恢复

系统调用函数:
在某些操作系统中,可以使用特定的系统调用函数来挂起(暂停)一个进程的执行。
例如,在UNIX/Linux系统中,kill()函数的某些信号可以用来挂起进程。挂起的进程可以通过其他系统调用函数来恢复执行。

进程挂起和恢复通常使用kill命令发送SIGSTOP和SIGCONT信号。

6.7、进程优先级调整

命令和函数:
可以使用nice命令或renice命令来设置或修改进程的优先级。
进程的优先级决定了它在系统资源分配时的优先级别,优先级高的进程更容易获得CPU等资源。

进程优先级的调整可以使用nice命令或renice命令。
# 以更高的优先级(更低的nice值)启动程序  
nice -n -10 ./my_program

6.8、前后台进程切换

命令操作: 在Linux中,可以使用&将进程置于后台运行,使用fg命令将后台进程调到前台运行,使用bg命令将挂起的后台进程继续在后台执行。jobs命令用于查看当前所有的后台任务。

注意事项

进程管理和操作的具体方法会根据不同的操作系统和编程语言而有所不同。
在进行进程操作时,需要考虑到进程的同步、互斥和死锁等问题,以确保系统的稳定性和可靠性。

7、线程与进程的区别差异

区别线程进程
定义线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。进程是系统进行资源分配和调度的一个独立单元,是应用程序运行的容器。
关系线程是进程的一部分,每个进程可以拥有多个线程。进程是线程的容器,一个线程只能属于一个进程。
资源共享线程共享进程的地址空间和资源,包括文件描述符和内存。进程拥有独立的内存空间、文件描述符等资源,进程之间通过IPC机制进行通信。
切换开销线程切换的开销较小,因为线程共享进程的资源,不需要保存和恢复整个进程的上下文。进程切换的开销较大,因为需要保存和恢复整个进程的上下文。
独立性线程相对独立,但共享进程的资源,因此线程间的通信和同步更为简单。进程完全独立,进程间的通信和同步需要显式的IPC机制。
管理复杂度线程的管理相对简单,因为它们是进程的一部分,共享相同的地址空间。进程的管理相对复杂,因为每个进程都有独立的资源,需要单独管理。

8、线程和进程相关的Linux指令

1.ps
用途:

查看当前系统中的进程状态。

常用选项:

ps -u $USER:查看当前用户的所有进程。
ps aux:显示系统中所有进程的详细信息。
ps -ef:以全格式显示当前所有的进程。

2.top
用途:

动态地显示系统中各个进程的资源占用情况,如CPU、内存等。

操作:

按q键退出,h键查看帮助。

3.htop
用途:

htop是top命令的增强版,提供了一个更为友好的用户界面和更多的交互功能。

安装:

在某些Linux发行版中可能需要手动安装(如使用sudo apt-get install htop命令)。

4.pgrep
用途:

通过进程名查找进程的PID(进程标识符)。

用法:

pgrep <process_name>

5.kill
用途:

通过PID终止进程。

用法:

kill <PID>,使用-9选项可以强制终止进程。

6.pkill
用途:

通过进程名终止进程。

用法:

pkill <process_name>,使用-9选项可以强制终止。

7.killall
用途:

通过进程名终止所有匹配的进程。

用法:

killall <process_name>

8.nice
用途:

在启动进程时指定其优先级。

用法:

nice -n <nice_value> <command><nice_value>的取值范围是-20(最高优先级)到19(最低优先级)。

9.renice
用途:

调整正在运行的进程的优先级。

用法:

renice -n <nice_value> -p <PID>

10.pstree
用途:

以树状图显示进程及其子线程的关系。

用法:

pstree -p(显示PID)或pstree -t(显示线程ID)

11.strace
用途:

跟踪系统调用和信号,也可以用于跟踪特定线程的系统调用。

用法:

strace -p <PID>(跟踪指定PID的进程及其线程的系统调用)。

12.pthread库函数(针对线程)

在编程层面,使用POSIX线程(pthread)库时,可以通过库提供的函数(如pthread_create、pthread_join、pthread_self等)来创建、管理和查看线程。

9、Linux多线程和多进程

9.1、多线程

9.1.1、定义与特点

定义:

多进程是指在一个系统中同时运行多个进程,每个进程都拥有独立的内存空间和系统资源。

特点:

1.独立性:每个进程都是独立的执行实体,拥有自己的内存空间、文件描述符等资源,互不影响。
2.并发性:多个进程可以同时执行,操作系统通过时间分片等机制实现并发。
3.开销较大:进程之间的切换和通信开销相对较大,因为需要复制或共享大量的资源。

9.1.2、应用场景

1.需要高隔离性的场景,如不同的用户任务或需要保护的数据。
2.长时间运行的任务,因为进程具有完整的生命周期管理。

9.1.3、相关API和函数
fork():用于创建新的子进程。
exec()系列函数:用于在子进程中加载并执行新的程序。
wait()waitpid():用于等待子进程结束并获取其退出状态。
getpid()getppid():用于获取当前进程的PID和父进程的PID。

9.2、多进程

9.2.1、定义与特点

定义:

多线程是指在同一个进程中并发执行的多个执行序列,每个线程都拥有自己的程序计数器、寄存器集合和栈空间,但它们共享同一个进程的地址空间和其他资源。

特点:

1.轻量级:线程的创建、销毁和切换开销较小,因为它们共享同一进程的内存空间和上下文。
2.并发执行:多个线程可以并发执行,提高程序的性能和响应速度。
3.共享资源:线程之间共享同一进程的资源,包括全局变量、静态变量、堆内存等。

9.2.2、应用场景

1.需要高效并发执行的场景,如服务器处理多个客户端请求。
2.复杂的GUI(图形用户界面)应用程序,其中多个线程可以处理不同的用户输入和事件。

9.2.3、相关API和函数
pthread_create():用于创建新的线程。
pthread_join():用于等待指定的线程结束。
pthread_detach():用于将线程设置为分离状态,使其结束时自动释放资源。
pthread_mutex_lock()pthread_mutex_unlock()等:用于线程同步,避免竞争条件和数据访问冲突。

9.3、多线程与多进程的比较

项目Value多进程
独立性低,线程共享进程的内存空间和资源高,每个进程拥有独立的内存空间和资源
开销较小,线程之间的切换和通信开销小较大,进程之间的切换和通信开销大
隔离性弱,一个线程的错误可能会影响其他线程强,一个进程的错误不会影响其他进程
应用场景需要高效并发执行的场景,如服务器处理多个客户端请求需要高隔离性的场景,如不同的用户任务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值