MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

进程从运行态到阻塞态的转换触发因素

2023-09-054.3k 阅读

进程状态概述

在深入探讨进程从运行态到阻塞态的转换触发因素之前,我们先来回顾一下进程的基本状态。进程在操作系统中通常有三种基本状态:运行态(Running)、就绪态(Ready)和阻塞态(Blocked)。运行态表示进程正在CPU上执行;就绪态意味着进程已经准备好运行,只要获得CPU资源就可以立即执行;而阻塞态则表明进程由于等待某些事件的发生而暂时无法运行。

进程状态转换图

进程状态之间的转换构成了一个状态转换图,这有助于我们直观地理解进程状态的变化。在这个图中,进程可以从就绪态转换到运行态,当CPU调度器选择了该进程时;进程从运行态可以转换到就绪态,例如当时间片用完或者有更高优先级的进程抢占CPU时;而从运行态转换到阻塞态则是我们接下来要重点探讨的内容。

资源请求相关触发因素

1. 等待I/O操作完成

在现代计算机系统中,I/O操作是非常常见且耗时的操作。进程在运行过程中,经常需要与外部设备进行数据交互,如读取文件、从网络接收数据、向打印机输出等。当进程发起一个I/O请求后,由于I/O设备的速度相对CPU来说非常慢,进程无法立即得到所需的数据或完成数据输出。为了不浪费CPU资源,操作系统会将该进程从运行态转换为阻塞态,让CPU可以去执行其他就绪态的进程。

以文件读取操作为例,在C语言中,我们可以使用标准库函数fopenfread等来进行文件操作。假设我们有如下代码:

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

int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    char buffer[1024];
    size_t result = fread(buffer, 1, sizeof(buffer), file);
    if (result == 0) {
        perror("Failed to read file");
        fclose(file);
        return 1;
    }

    fclose(file);
    // 对读取到的数据进行处理
    return 0;
}

在执行fread函数时,进程会等待磁盘设备将文件数据读取到内存中。在这个等待过程中,操作系统会将该进程转换为阻塞态。操作系统通过设备驱动程序来管理I/O设备,当I/O操作完成后,设备驱动程序会产生一个中断信号。操作系统接收到这个中断信号后,会将等待该I/O操作的进程从阻塞态转换为就绪态,等待CPU调度执行。

2. 等待共享资源

在多进程环境中,经常会存在一些共享资源,如共享内存、信号量、互斥锁等。当一个进程需要访问共享资源时,如果该资源当前正被其他进程占用,那么该进程就需要等待。此时,进程会从运行态转换为阻塞态。

以互斥锁为例,在POSIX线程库(pthread)中,我们可以使用互斥锁来保护共享资源。假设我们有两个进程都需要访问一个共享变量,代码如下:

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

// 共享变量
int shared_variable = 0;
// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *thread_function(void *arg) {
    // 加锁
    pthread_mutex_lock(&mutex);
    // 访问共享资源
    shared_variable++;
    printf("Thread incremented shared variable: %d\n", shared_variable);
    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // 创建线程1
    if (pthread_create(&thread1, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread 1");
        return 1;
    }

    // 创建线程2
    if (pthread_create(&thread2, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread 2");
        return 1;
    }

    // 等待线程1结束
    if (pthread_join(thread1, NULL) != 0) {
        perror("Failed to join thread 1");
        return 1;
    }

    // 等待线程2结束
    if (pthread_join(thread2, NULL) != 0) {
        perror("Failed to join thread 2");
        return 1;
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

当一个线程调用pthread_mutex_lock尝试获取互斥锁时,如果互斥锁已经被其他线程持有,那么该线程(进程)就会被阻塞,进入阻塞态。直到持有互斥锁的线程调用pthread_mutex_unlock释放互斥锁,被阻塞的线程才会被唤醒,从阻塞态转换为就绪态。

3. 等待缓冲区空间

在一些数据传输场景中,如网络通信、管道通信等,进程需要将数据发送到缓冲区中。如果缓冲区已满,进程就无法立即将数据写入,此时进程会进入阻塞态,等待缓冲区有足够的空间。

以管道通信为例,在Linux系统中,我们可以使用pipe函数创建一个管道。假设有如下代码:

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

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t cpid = fork();
    if (cpid == -1) {
        perror("fork");
        return 1;
    } else if (cpid == 0) { // 子进程
        close(pipefd[0]); // 关闭读端
        char *message = "Hello, parent!";
        ssize_t bytes_written = write(pipefd[1], message, sizeof(message));
        if (bytes_written == -1) {
            perror("write");
            return 1;
        }
        close(pipefd[1]); // 关闭写端
        exit(0);
    } else { // 父进程
        close(pipefd[1]); // 关闭写端
        char buffer[1024];
        ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
        if (bytes_read == -1) {
            perror("read");
            return 1;
        }
        buffer[bytes_read] = '\0';
        printf("Parent received: %s\n", buffer);
        close(pipefd[0]); // 关闭读端
        wait(NULL); // 等待子进程结束
    }
    return 0;
}

在子进程向管道写数据时,如果管道缓冲区已满,write操作会阻塞,子进程进入阻塞态。只有当父进程从管道中读取数据,使缓冲区有了空间后,子进程才会被唤醒,继续执行写操作。

同步相关触发因素

1. 等待信号量

信号量是一种用于进程同步的机制,它通过一个计数器来控制对共享资源的访问。当进程需要访问共享资源时,它会尝试获取信号量。如果信号量的值大于0,进程可以成功获取信号量,将信号量的值减1,并继续执行;如果信号量的值为0,进程就会被阻塞,进入阻塞态。

以POSIX信号量为例,假设我们有如下代码:

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

sem_t semaphore;

void *thread_function(void *arg) {
    // 获取信号量
    sem_wait(&semaphore);
    printf("Thread acquired semaphore\n");
    // 模拟一些操作
    sleep(1);
    // 释放信号量
    sem_post(&semaphore);
    return NULL;
}

int main() {
    // 初始化信号量
    if (sem_init(&semaphore, 0, 1) == -1) {
        perror("sem_init");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建线程1
    if (pthread_create(&thread1, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread 1");
        return 1;
    }

    // 创建线程2
    if (pthread_create(&thread2, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread 2");
        return 1;
    }

    // 等待线程1结束
    if (pthread_join(thread1, NULL) != 0) {
        perror("Failed to join thread 1");
        return 1;
    }

    // 等待线程2结束
    if (pthread_join(thread2, NULL) != 0) {
        perror("Failed to join thread 2");
        return 1;
    }

    // 销毁信号量
    sem_destroy(&semaphore);
    return 0;
}

在这个例子中,当一个线程调用sem_wait获取信号量时,如果信号量的值已经为0(表示共享资源已被占用),该线程就会被阻塞。当另一个线程调用sem_post释放信号量,使信号量的值变为1时,被阻塞的线程会被唤醒,从阻塞态转换为就绪态。

2. 等待条件变量

条件变量是另一种进程同步机制,它通常与互斥锁一起使用。进程在等待某个条件满足时,会通过条件变量进入阻塞态。

以POSIX条件变量为例,假设我们有如下代码:

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

// 共享变量
int shared_value = 0;
// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 条件变量
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;

void *thread_function(void *arg) {
    // 加锁
    pthread_mutex_lock(&mutex);
    // 等待条件满足
    while (shared_value != 1) {
        pthread_cond_wait(&condition, &mutex);
    }
    printf("Thread woke up, shared value is %d\n", shared_value);
    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1;

    // 创建线程1
    if (pthread_create(&thread1, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread 1");
        return 1;
    }

    // 模拟一些操作
    sleep(2);

    // 加锁
    pthread_mutex_lock(&mutex);
    shared_value = 1;
    // 唤醒等待的线程
    pthread_cond_signal(&condition);
    // 解锁
    pthread_mutex_unlock(&mutex);

    // 等待线程1结束
    if (pthread_join(thread1, NULL) != 0) {
        perror("Failed to join thread 1");
        return 1;
    }

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condition);
    return 0;
}

在这个例子中,线程调用pthread_cond_wait时,会先释放互斥锁(防止死锁),然后进入阻塞态。当主线程调用pthread_cond_signal唤醒条件变量时,被阻塞的线程会重新获取互斥锁,并检查条件是否满足。如果满足,线程继续执行;否则,线程可能会再次进入阻塞态。

3. 等待事件通知

在一些异步编程模型中,进程可能会等待特定事件的通知。例如,在基于事件驱动的编程中,进程会注册对某些事件的回调函数。当事件发生时,进程会被唤醒并执行相应的回调函数。在等待事件发生的过程中,进程处于阻塞态。

以Linux系统中的epoll机制为例,epoll是一种高效的I/O多路复用技术,常用于网络编程。假设我们有一个简单的服务器程序,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define MAX_EVENTS 10

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(8080);

    if (bind(listenfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    if (listen(listenfd, 10) < 0) {
        perror("listen failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    int epollfd = epoll_create1(0);
    if (epollfd == -1) {
        perror("epoll_create1");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    struct epoll_event event;
    event.data.fd = listenfd;
    event.events = EPOLLIN;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) == -1) {
        perror("epoll_ctl: listenfd");
        close(listenfd);
        close(epollfd);
        exit(EXIT_FAILURE);
    }

    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listenfd) {
                socklen_t len = sizeof(cliaddr);
                connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);
                if (connfd == -1) {
                    perror("accept");
                    continue;
                }

                int flags = fcntl(connfd, F_GETFL, 0);
                fcntl(connfd, F_SETFL, flags | O_NONBLOCK);

                event.data.fd = connfd;
                event.events = EPOLLIN | EPOLLET;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) == -1) {
                    perror("epoll_ctl: connfd");
                    close(connfd);
                }
            } else {
                int fd = events[i].data.fd;
                char buffer[1024];
                ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
                if (n == -1) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        perror("recv");
                        close(fd);
                    }
                } else if (n == 0) {
                    close(fd);
                } else {
                    buffer[n] = '\0';
                    printf("Received: %s\n", buffer);
                }
            }
        }
    }

    close(listenfd);
    close(epollfd);
    return 0;
}

在这个服务器程序中,调用epoll_wait时,进程会进入阻塞态,等待有新的连接到来或者已有连接上有数据可读。当有事件发生时,epoll_wait返回,进程从阻塞态转换为就绪态,处理相应的事件。

其他触发因素

1. 等待定时器到期

在一些应用场景中,进程需要等待特定的时间间隔后执行某些操作。操作系统通常提供了定时器机制来满足这种需求。当进程设置了一个定时器后,它会进入阻塞态,等待定时器到期。

以Linux系统中的timerfd为例,假设我们有如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/timerfd.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

int main() {
    int timerfd_fd = timerfd_create(CLOCK_REALTIME, 0);
    if (timerfd_fd == -1) {
        perror("timerfd_create");
        return 1;
    }

    struct itimerspec new_value;
    memset(&new_value, 0, sizeof(new_value));
    new_value.it_value.tv_sec = 5; // 5秒后首次到期
    new_value.it_interval.tv_sec = 2; // 之后每2秒到期一次

    if (timerfd_settime(timerfd_fd, 0, &new_value, NULL) == -1) {
        perror("timerfd_settime");
        close(timerfd_fd);
        return 1;
    }

    char buffer[8];
    while (1) {
        ssize_t n = read(timerfd_fd, buffer, sizeof(buffer));
        if (n == -1) {
            perror("read");
            break;
        }
        printf("Timer expired\n");
    }

    close(timerfd_fd);
    return 0;
}

在这个例子中,调用readtimerfd读取数据时,如果定时器还未到期,进程会进入阻塞态。当定时器到期时,read函数返回,进程从阻塞态转换为就绪态,处理定时器到期事件。

2. 等待子进程结束

在一些情况下,父进程需要等待子进程执行完毕,获取子进程的执行结果。当父进程调用waitwaitpid函数时,如果子进程还未结束,父进程会进入阻塞态。

wait函数为例,假设我们有如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t cpid = fork();
    if (cpid == -1) {
        perror("fork");
        return 1;
    } else if (cpid == 0) { // 子进程
        printf("Child process is running\n");
        sleep(3);
        printf("Child process is exiting\n");
        exit(0);
    } else { // 父进程
        printf("Parent process is waiting for child\n");
        int status;
        pid_t wpid = wait(&status);
        if (wpid == -1) {
            perror("wait");
            return 1;
        }
        if (WIFEXITED(status)) {
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

在这个例子中,父进程调用wait函数后,如果子进程还在运行,父进程就会进入阻塞态。当子进程结束时,wait函数返回,父进程从阻塞态转换为就绪态,获取子进程的退出状态。

3. 系统调用阻塞

操作系统提供了各种各样的系统调用,有些系统调用在执行过程中可能会因为等待某些条件而阻塞。例如,pause系统调用会使调用进程挂起,直到收到一个信号。

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

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    signal(SIGINT, signal_handler);
    printf("Process is pausing...\n");
    pause();
    printf("Process resumed\n");
    return 0;
}

在这个例子中,调用pause后,进程进入阻塞态。当进程收到SIGINT信号(例如用户按下Ctrl+C)时,信号处理函数被调用,pause返回,进程从阻塞态转换为就绪态。

通过对以上各种触发因素的深入分析,我们可以更全面地理解进程从运行态到阻塞态的转换机制,这对于编写高效、稳定的多进程程序以及深入理解操作系统的进程管理机制都具有重要意义。无论是资源请求、同步操作还是其他各种场景,操作系统都通过精心设计的机制来管理进程状态的转换,以充分利用系统资源,提高系统的整体性能。