操作系统(Linux实战)-进程创建、同步与锁、通信、调度算法-学习笔记

1. 进程的基础概念

1.1 进程是什么?

定义

  • 进程是操作系统管理的一个程序实例。它包含程序代码及其当前活动的状态。每个进程有自己的内存地址空间,拥有独立的栈、堆、全局变量等。操作系统通过进程来分配资源(如 CPU 时间、内存等)并管理任务的执行。

进程 vs 程序

  • 程序:静态的代码和数据,存储在磁盘上。
  • 进程:程序的一个运行实例,包括程序的代码、活动状态、资源等。

1.2 进程的内存布局

一个典型进程的内存分布如下:

内存段 描述
(Stack) 存储函数调用时的局部变量、函数调用链等。栈从高地址向低地址增长。
空间(Gap) 堆和栈之间未分配的内存区域,用于防止二者相互干扰。
(Heap) 用于动态内存分配(如使用 malloc()),堆从低地址向高地址增长。
BSS段(BSS Segment) 存储未初始化的全局变量和静态变量。
数据段(Data Segment) 存储已初始化的全局变量和静态变量。
代码段(Text Segment) 存储程序的代码,即指令。
c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 全局变量(已初始化) - 数据段
int global_initialized = 42;

// 静态变量(未初始化) - BSS 段
static int static_uninitialized;

// 函数 - 代码段
void display_addresses() {
    // 局部变量 - 栈段
    int local_variable = 0;
    
    printf("代码段 (Text Segment): %p\n", (void*)display_addresses);
    printf("数据段 (Data Segment) (已初始化全局变量): %p\n", (void*)&global_initialized);
    printf("BSS 段 (BSS Segment) (未初始化静态变量): %p\n", (void*)&static_uninitialized);
    printf("栈段 (Stack Segment) (局部变量): %p\n", (void*)&local_variable);
}

int main() {
    // 堆内存 - 堆段
    int* heap_variable = (int*)malloc(10 * sizeof(int));

    // 显示各内存段的地址
    display_addresses();

    printf("堆段 (Heap Segment) (动态分配内存): %p\n", (void*)heap_variable);

    // 再次分配内存,看看堆地址是否变化
    int* heap_variable2 = (int*)malloc(10 * sizeof(int));
    printf("堆段 (Heap Segment) (再次分配内存): %p\n", (void*)heap_variable2);

    // 释放内
    free(heap_variable);
    free(heap_variable2);

    return 0;
}

运行结果:

代码段 (Text Segment): 0x55d05bad21a9

数据段 (Data Segment) (已初始化全局变量): 0x55d05bad5010

BSS 段 (BSS Segment) (未初始化静态变量): 0x55d05bad5018

栈段 (Stack Segment) (局部变量): 0x7ffc49a5c084

堆段 (Heap Segment) (动态分配内存): 0x55d05c49c2a0

堆段 (Heap Segment) (再次分配内存): 0x55d05c49c6e0

1.3 进程的生命周期

进程在操作系统中的生命周期包括以下几个状态:

进程状态 描述
新建(New) 进程被创建,操作系统为其分配资源。
就绪(Ready) 进程已经准备好运行,等待调度器分配 CPU。
运行(Running) 进程正在 CPU 上执行指令。
阻塞(Blocked/Waiting) 进程在等待某些条件(如 I/O 操作完成),无法继续执行。
终止(Terminated) 进程执行完毕或被强制终止,操作系统回收其资源。

状态转换图

新建 (New)就绪 (Ready) :

当一个新进程被创建且已经准备好运行时,它从"新建"状态转变为"就绪"状态。

就绪 (Ready)运行 (Running) :

当调度程序选择一个就绪状态的进程来运行时,它从"就绪"状态转变为"运行"状态。

运行 (Running)阻塞 (Blocked) :

如果运行中的进程需要等待某些资源(如I/O操作),它会从"运行"状态转变为"阻塞"状态。

阻塞 (Blocked)就绪 (Ready) :

当阻塞状态的进程等待的事件发生后,它会重新进入"就绪"状态,准备再次运行。

运行 (Running)就绪 (Ready) :

如果进程由于某种原因没有完成执行而被中断,它将从"运行"状态返回到"就绪"状态,等待下次被调度。

运行 (Running)终止 (Term) :

当进程完成执行或被强制终止时,它从"运行"状态转变为"终止"状态。

2. 进程的创建与管理

进程的创建与管理是操作系统并发机制的核心。在 Unix/Linux 系统中,fork()exec() 是创建和管理进程的两个主要系统调用。

2.1 使用 fork() 创建进程

fork() 是 Unix/Linux 中用于创建新进程的系统调用。它会复制当前进程,创建一个新的子进程。子进程几乎完全与父进程相同,但它们有独立的内存空间和资源。

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建一个子进程

    if (pid < 0) {
        // fork() 失败
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("This is the child process with PID: %d\n", getpid());
    } else {
        // 父进程代码
        printf("This is the parent process with PID: %d\n", getpid());
        printf("Created child process with PID: %d\n", pid);
    }

    return 0;
}

运行结果:

This is the parent process with PID: 1683911

Created child process with PID: 1683912

liber@liber-VMware-Virtual-Platform:/home/c$ This is the child process with PID: 1683912

解释

  • fork() 创建一个新的子进程,子进程是父进程的副本。
  • pid_t pid = fork()fork() 返回两次,一次在父进程中返回子进程的 PID,一次在子进程中返回 0。通过判断 pid 的值,可以区分当前代码是运行在父进程还是子进程中。

2.2 使用 exec() 系列函数替换进程映像

在创建了子进程后,常常需要在子进程中执行不同的程序,这时可以使用 exec() 系列函数来替换当前进程的映像。exec() 系列函数包括 execl()execv()execlp()execvp() 等。

下面是关于 exec 系列函数概述的表格,展示了每个函数的使用方式、参数类型及其特点:

函数名称 参数类型 程序路径 查找方式 说明
execl() 参数列表(可变参数) 需要指定完整路径 不查找路径 通过传递一个参数列表执行指定路径的程序,参数列表必须以NULL结尾。
execv() 参数数组 需要指定完整路径 不查找路径 通过传递一个参数数组执行指定路径的程序,参数数组最后一个元素必须是NULL
execlp() 参数列表(可变参数) 只需指定程序名称 使用PATH查找 通过传递一个参数列表执行指定名称的程序,系统在PATH环境变量中查找该程序的路径。参数列表必须以NULL结尾。
execvp() 参数数组 只需指定程序名称 使用PATH查找 通过传递一个参数数组执行指定名称的程序,系统在PATH环境变量中查找该程序的路径。参数数组最后一个元素必须是NULL
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
    pid_t pid;

    // 使用 execl() 执行 /bin/ls
    pid = fork();
    if (pid == 0) {
        // 在子进程中执行
        printf("Child process (PID: %d) using execl() to execute /bin/ls -l\n", getpid());
        execl("/bin/ls", "ls", "-l", NULL);  // 执行 ls -l
        perror("execl failed");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程代码
        wait(NULL);  // 等待子进程完成
        printf("execl() Child process completed.\n");
    }

    // 使用 execv() 执行 /bin/echo
    pid = fork();
    if (pid == 0) {
        // 在子进程中执行
        printf("Child process (PID: %d) using execv() to execute /bin/echo 'Hello, World!'\n", getpid());
        char *args1[] = {"/bin/echo", "Hello,", "World!", NULL};
        execv("/bin/echo", args1);  // 执行 echo "Hello, World!"
        perror("execv failed");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程代码
        wait(NULL);  // 等待子进程完成
        printf("execv() Child process completed.\n");
    }

    // 使用 execlp() 执行 ls
    pid = fork();
    if (pid == 0) {
        // 在子进程中执行
        printf("Child process (PID: %d) using execlp() to execute ls -a\n", getpid());
        execlp("ls", "ls", "-a", NULL);  // 执行 ls -a
        perror("execlp failed");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程代码
        wait(NULL);  // 等待子进程完成
        printf("execlp() Child process completed.\n");
    }

    // 使用 execvp() 执行 echo
    pid = fork();
    if (pid == 0) {
        // 在子进程中执行
        printf("Child process (PID: %d) using execvp() to execute echo 'This is execvp!'\n", getpid());
        char *args2[] = {"echo", "This", "is", "execvp!", NULL};
        execvp("echo", args2);  // 执行 echo "This is execvp!"
        perror("execvp failed");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程代码
        wait(NULL);  // 等待子进程完成
        printf("execvp() Child process completed.\n");
    }

    printf("All child processes completed. Parent process exiting.\n");
    return 0;
}

运行结果:

Child process (PID: 1697610) using execl() to execute /bin/ls -l

总计 24

-rwxrwxr-x 1 liber liber 16424 8月 13 22:58 code

-rw-rw-r-- 1 liber liber 2346 8月 13 22:58 code.c

execl() Child process completed.

Child process (PID: 1697612) using execv() to execute /bin/echo 'Hello, World!'

Hello, World!

execv() Child process completed.

Child process (PID: 1697614) using execlp() to execute ls -a

. ... code code.c

execlp() Child process completed.

Child process (PID: 1697616) using execvp() to execute echo 'This is execvp!'

This is execvp!

execvp() Child process completed.

All child processes completed. Parent process exiting.

解释

  • 在第一个子进程中,使用execl()函数执行/bin/ls命令。参数列表包括路径/bin/ls、程序名称ls以及命令行参数-l。如果成功,子进程会替换为ls -l的执行。
  • 在第二个子进程中,使用execv()函数执行/bin/echo命令。参数通过一个数组args1传递,包含程序路径/bin/echo及参数"Hello," "World!"。如果成功,子进程会输出Hello, World!
  • 在第三个子进程中,使用execlp()函数执行ls -a命令。只需要提供程序名称ls,系统会在PATH环境变量中查找ls的路径。成功时,子进程会替换为ls -a的执行。
  • 在第四个子进程中,使用execvp()函数执行echo命令。参数通过数组args2传递,包括程序名称echo及参数"This", "is", "execvp!"。系统会在PATH环境变量中查找echo的路径。成功时,子进程会输出This is execvp!

3. 进程的同步与等待

3.1 wait()waitpid() 函数

父进程可以使用 wait()waitpid() 函数来等待子进程终止,并获取子进程的退出状态。waitpid()wait() 的增强版本,允许你等待特定的子进程或以非阻塞方式等待。它不仅可以像 wait() 一样等待任意一个子进程结束,还可以通过传递子进程的 PID 来等待特定的子进程。

wait:

c 复制代码
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid == 0) {
        // 子进程部分
        printf("Child process with PID: %d\n", getpid());
        sleep(2);  // 模拟子进程工作
    } else if (pid > 0) {
        // 父进程部分
        printf("Parent waiting using wait().\n");
        wait(NULL);  // 使用 wait() 等待子进程
        printf("Child process has terminated.\n");
    }

    return 0;
}

运行结果:

Parent waiting using wait().

Child process with PID: 1777167

Child process has terminated.

解释

  • 子进程执行 sleep(2) 来模拟工作,父进程使用 wait() 等待子进程结束。

waitpid:

c 复制代码
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid == 0) {
        // 子进程部分
        printf("Child process with PID: %d\n", getpid());
        sleep(3);  // 模拟子进程工作
    } else if (pid > 0) {
        // 父进程部分
        printf("Parent waiting using waitpid().\n");
        waitpid(pid, NULL, 0);  // 使用 waitpid() 等待特定子进程
        printf("Child process has terminated.\n");
    }

    return 0;
}

运行结果:

Parent waiting using waitpid().

Child process with PID: 1779327

Child process has terminated.

解释

  • 子进程执行 sleep(3) 来模拟工作,父进程使用 waitpid() 等待这个特定的子进程。

4. 进程间通信(IPC)

进程间通信是多个进程之间交换数据的机制。常见的 IPC 机制包括管道(Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)和信号量(Semaphores)。

4.1 管道(Pipe)

管道是一种常用的 IPC 机制,允许一个进程向另一个进程传递数据。管道分为匿名管道和命名管道(FIFO)。

**匿名管道:**适用于有亲缘关系的进程间通信,比如父子进程。它的特点是临时性,不会在文件系统中留下痕迹。

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    int fd[2];
    char buffer[30];

    pipe(fd);  // 创建管道
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程:向管道写入数据
        write(fd[1], "Hello from child", 17);
    } else if (pid > 0) {
        // 父进程:从管道读取数据
        read(fd[0], buffer, sizeof(buffer));
        printf("Received from child process: %s\n", buffer);
    }

    return 0;
}

运行结果:

Received from child process: Hello from child

解释

  • pipe(fd) 创建一个匿名管道,fd[0] 是读端,fd[1] 是写端。
  • 子进程写入数据,父进程从管道读取数据,实现简单的进程间通信。

**命名管道(FIFO):**是一种特殊的文件,它允许无亲缘关系的进程通过管道文件进行通信。命名管道在文件系统中存在,可以由任何进程打开和使用。

创建命名管道并写入数据:

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

int main() {
    char *fifo = "/tmp/my_fifo";  // 定义FIFO的路径

    // 创建命名管道
    mkfifo(fifo, 0666);

    // 打开FIFO的写端
    int fd = open(fifo, O_WRONLY);
    
    char message[] = "Hello from writer!";
    write(fd, message, strlen(message) + 1);  // 写入数据到FIFO

    close(fd);  // 关闭FIFO

    return 0;
}

运行结果:

光标一直闪烁,等待被消费。

读取命名管道中的数据:

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    char *fifo = "/tmp/my_fifo";  // 定义FIFO的路径
    char buffer[100];

    // 打开FIFO的读端
    int fd = open(fifo, O_RDONLY);
    
    read(fd, buffer, sizeof(buffer));  // 从FIFO中读取数据
    printf("Reader process received: %s\n", buffer);

    close(fd);  // 关闭FIFO

    // 删除命名管道文件
    unlink(fifo);

    return 0;
}

运行结果:

Reader process received: Hello from writer!

解释:

  • 需要编译两个文件,打开两个终端编译运行两个不同的代码文件,一个生产,一个消费。

  • 一个进程通过打开命名管道的写端来发送数据,另一个进程通过打开命名管道的读端来接收数据。

  • 读完数据后,使用 unlink(fifo) 删除命名管道文件

4.2 消息队列(Message Queue)

消息队列是一种进程间通信(IPC)机制,允许进程通过消息的形式进行异步通信。与管道不同,消息队列支持有序存储消息,并允许进程以不同的优先级发送和接收消息。消息队列是持久的,即使创建消息队列的进程退出,消息队列仍然存在,直到显式删除为止。

以下是包含操作说明的消息队列操作表格:

函数 参数 参数描述 操作 返回值
msgget() key 消息队列的键值,唯一标识消息队列。 创建或获取消息队列 成功时返回消息队列标识符,失败时返回 -1
msgflg 标志位,用于设置权限和操作选项,如 IPC_CREAT 表示创建消息队列。
msgsnd() msqid 消息队列标识符(通过 msgget() 获取)。 向消息队列发送消息 成功时返回 0,失败时返回 -1
msgp 指向消息结构的指针,包含消息类型和消息正文。
msgsz 消息正文的大小。
msgflg 控制操作的标志位,如 IPC_NOWAIT 表示非阻塞发送。
msgrcv() msqid 消息队列标识符。 从消息队列接收消息 成功时返回接收到的消息大小,失败时返回 -1
msgp 指向存储接收到消息的结构的指针。
msgsz 消息正文的大小。
msgtyp 指定要接收的消息类型,0 表示接收队列中的第一个消息。
msgflg 控制操作的标志位,如 IPC_NOWAIT 表示非阻塞接收。
msgctl() msqid 消息队列标识符。 控制消息队列操作 成功时返回 0,失败时返回 -1
cmd 指定要执行的操作,如 IPC_RMID 删除消息队列,IPC_STAT 获取消息队列信息。
buf 可选参数,用于存储或传递消息队列的状态信息。

详细操作说明:

  1. msgget():

    操作: 用于创建一个新的消息队列或获取一个现有的消息队列。

    典型用法 : 如果消息队列不存在且设置了 IPC_CREAT 标志,则会创建一个新的消息队列。否则,返回现有的消息队列标识符。

  2. msgsnd():

    操作: 将消息发送到指定的消息队列中。

    典型用法 : 将消息结构 msgp 中的消息添加到 msqid 标识的队列中。如果设置了 IPC_NOWAIT,则在队列已满时不会阻塞。

  3. msgrcv():

    操作: 从指定的消息队列中接收消息。

    典型用法 : 从 msqid 标识的队列中接收消息,并将其存储在 msgp 指向的结构中。如果 msgtyp0,接收队列中的第一个消息;如果 msgtyp 为正数,接收该类型的消息。

  4. msgctl():

    操作: 执行消息队列的控制操作,例如删除消息队列或查询队列的状态。

    典型用法 : 使用 IPC_RMID 标志删除消息队列,或使用 IPC_STAT 获取队列的状态信息。

发送消息的程序(sender.c)

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

#define MAX 100

// 定义消息结构
struct mesg_buffer {
    long mesg_type;
    char mesg_text[MAX];
} message;

int main() {
    key_t key;
    int msgid;

    // 生成唯一的键
    key = ftok("progfile", 65);

    // 创建消息队列并返回标识符
    msgid = msgget(key, 0666 | IPC_CREAT);

    // 消息类型设为 1
    message.mesg_type = 1;

    while(1) {
        printf("Enter a message to send: ");
        fgets(message.mesg_text, MAX, stdin);

        // 向消息队列发送消息
        msgsnd(msgid, &message, sizeof(message), 0);

        // 如果输入 "exit",结束聊天
        if (strncmp(message.mesg_text, "exit", 4) == 0) {
            break;
        }
    }

    return 0;
}

运行结果:

liber@liber-VMware-Virtual-Platform:/home/c$ gcc sender.c -o sender

liber@liber-VMware-Virtual-Platform:/home/c$ ./sender

Enter a message to send: hello

Enter a message to send: exit

解释:

  • msgget()用于创建或获取消息队列。使用 ftok() 函数生成一个唯一的键值,用于标识消息队列。

  • msgsnd()sender 程序中用于发送消息。消息类型设置为 1,消息内容通过用户输入获取。

接收消息的程序(receiver.c)

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

#define MAX 100

// 定义消息结构
struct mesg_buffer {
    long mesg_type;
    char mesg_text[MAX];
} message;

int main() {
    key_t key;
    int msgid;

    // 生成唯一的键
    key = ftok("progfile", 65);

    // 创建消息队列并返回标识符
    msgid = msgget(key, 0666 | IPC_CREAT);

    while(1) {
        // 从消息队列中接收消息
        msgrcv(msgid, &message, sizeof(message), 1, 0);

        printf("Received message: %s", message.mesg_text);

        // 如果接收到 "exit",结束聊天
        if (strncmp(message.mesg_text, "exit", 4) == 0) {
            // 删除消息队列
            msgctl(msgid, IPC_RMID, NULL);
            break;
        }
    }

    return 0;
}

运行结果:

liber@liber-VMware-Virtual-Platform:/home/c$ gcc receiver.c -o receiver

liber@liber-VMware-Virtual-Platform:/home/c$ ./receiver

Received message: hello

Received message: exit

解释:

  • msgrcv()receiver 程序中用于接收消息。它会阻塞,直到接收到类型为 1 的消息。
  • msgctl()当接收到的消息内容为 "exit" 时,使用 msgctl() 函数删除消息队列,清理 IPC 资源。

4.3共享内存(Shared Memory)

共享内存是一种高效的进程间通信(IPC)机制,允许多个进程直接访问同一块内存区域,从而实现数据的快速共享和交换。共享内存是所有 IPC 机制中最快的一种,因为数据不需要在进程之间复制,而是所有参与的进程可以直接访问同一片物理内存,以下是共享内存操作的基本函数及其用途。

函数 参数 操作 描述
shmget() key: 共享内存段的唯一标识符,通常由 ftok() 生成。 创建或获取共享内存段 创建或获取共享内存段,并返回一个标识符(shmid)。如果内存段不存在且设置了 IPC_CREAT,则创建一个新的共享内存段。
size: 共享内存段的大小(以字节为单位)。
shmflg: 标志位,常用值包括 IPC_CREAT 用于创建新内存段,0666 设置权限。
shmat() shmid: 共享内存段的标识符,由 shmget() 返回。 附加共享内存段到进程的地址空间 将共享内存段附加到当前进程的地址空间中,使得进程可以访问该内存。返回一个指向共享内存的指针。
shmaddr: 附加到的内存地址,通常为 NULL 让系统自动选择地址。
shmflg: 操作标志位,通常为 0(读写),也可以设置为 SHM_RDONLY 只读。
shmdt() shmaddr: 要分离的共享内存段的地址,必须是 shmat() 返回的地址。 从进程的地址空间分离共享内存段 将共享内存段从当前进程的地址空间分离,之后进程不能再访问该内存。
shmctl() shmid: 共享内存段的标识符。 控制共享内存段 控制共享内存段的行为,例如删除共享内存段以释放系统资源,或者获取共享内存段的状态信息。
cmd: 控制命令,如 IPC_RMID(删除共享内存段)或 IPC_STAT(获取状态信息)。
buf: 一个 struct shmid_ds 类型的缓冲区,用于存储或传递内存段的状态信息。

写入共享内存:

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main() {
    // 生成唯一的键值
    key_t key = ftok("shmfile", 65);

    // 创建共享内存段,返回标识符
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

    // 将共享内存段附加到进程的地址空间
    char *str = (char*) shmat(shmid, (void*)0, 0);

    // 向共享内存写入数据
    strcpy(str, "Hello from Process A!");

    printf("Data written to shared memory: %s\n", str);

    // 分离共享内存段
    shmdt(str);

    return 0;
}

运行结果:

Data written to shared memory: Hello from Process A!

解释:

**shmget()**创建或获取一个共享内存段,大小为 1024 字节。IPC_CREAT 标志表示如果共享内存段不存在则创建它。

**shmat()**将共享内存段附加到进程的地址空间,使得进程可以访问该内存。返回一个指向该内存段的指针。

读取共享内存:

c 复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
    // 生成与进程 A 相同的键值
    key_t key = ftok("shmfile", 65);

    // 获取共享内存段的标识符
    int shmid = shmget(key, 1024, 0666);

    // 将共享内存段附加到进程的地址空间
    char *str = (char*) shmat(shmid, (void*)0, 0);

    // 读取共享内存中的数据
    printf("Data read from shared memory: %s\n", str);

    // 分离共享内存段
    shmdt(str);

    // 删除共享内存段
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

运行结果:

Data read from shared memory: Hello from Process A!

解释:

**shmdt()**从进程的地址空间分离共享内存段。

**shmctl()**删除共享内存段,释放资源。

4.4 信号量(Semaphores)

信号量是一种用于进程或线程间同步的机制,用来控制对共享资源的访问。信号量通过维护一个计数器来控制多个进程或线程对临界资源的访问。信号量可以用于解决竞态条件问题,确保多个进程或线程在并发访问共享资源时不会产生冲突。

函数 参数 参数描述 操作 描述
sem_init() sem 指向信号量对象的指针,用于初始化信号量。 初始化信号量 初始化一个信号量,设置其初始值。根据 pshared 的值决定信号量用于线程间同步还是进程间共享。
pshared 指定信号量的作用范围,0信号量用于线程间同步,非 0信号量在进程间共享。
value 设置信号量的初始值,表示资源的初始可用数量。
sem_wait() sem 指向信号量对象的指针,表示要等待的信号量。 等待信号量 执行 P 操作。如果信号量值大于 0,信号量值减 1 并继续执行;如果信号量值为 0,则阻塞,直到信号量值大于 0。
sem_post() sem 指向信号量对象的指针,表示要释放的信号量。 释放信号量 执行 V 操作。将信号量的值加 1,如果有进程或线程在等待该信号量,则唤醒其中一个。
sem_destroy() sem 指向信号量对象的指针,用于销毁信号量。 销毁信号量 销毁信号量并释放与其相关的资源。仅适用于未用于进程间共享的信号量。

使用信号量同步两个线程:

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;

void* thread_func1(void* arg) {
    printf("Thread 1: Waiting for semaphore...\n");
    sem_wait(&sem);  // 等待信号量
    printf("Thread 1: Inside critical section.\n");
    sem_post(&sem);  // 释放信号量
    return NULL;
}

void* thread_func2(void* arg) {
    printf("Thread 2: Waiting for semaphore...\n");
    sem_wait(&sem);  // 等待信号量
    printf("Thread 2: Inside critical section.\n");
    sem_post(&sem);  // 释放信号量
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化信号量,初始值为 1
    sem_init(&sem, 0, 1);

    // 创建两个线程
    pthread_create(&t1, NULL, thread_func1, NULL);
    pthread_create(&t2, NULL, thread_func2, NULL);

    // 等待两个线程完成
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // 销毁信号量
    sem_destroy(&sem);

    return 0;
}

liber@liber-VMware-Virtual-Platform:/home/c$ ./receiver

Thread 1: Waiting for semaphore...

Thread 1: Inside critical section.

Thread 2: Waiting for semaphore...

Thread 2: Inside critical section.

liber@liber-VMware-Virtual-Platform:/home/c$ ./receiver

Thread 1: Waiting for semaphore...

Thread 2: Waiting for semaphore...

Thread 2: Inside critical section.

Thread 1: Inside critical section.

liber@liber-VMware-Virtual-Platform:/home/c$ ./receiver

Thread 2: Waiting for semaphore...

Thread 2: Inside critical section.

Thread 1: Waiting for semaphore...

Thread 1: Inside critical section.

解释:

  • **sem_init()**初始化信号量 sem,初始值设置为 1,表示资源最开始是可用的。第二个参数为 0,表示这是一个线程级别的信号量。

  • **sem_wait()**在每个线程的临界区(Critical Section)前调用 sem_wait(),等待信号量。如果信号量值为 1,则进入临界区并将信号量值减为 0;如果信号量值为 0,线程将阻塞,直到信号量变为 1。

  • **sem_post()**在临界区操作完成后调用 sem_post(),释放信号量,将信号量值加 1。如果有其他线程在等待信号量,它们将被唤醒。

  • **sem_destroy()**在程序结束时,销毁信号量 sem,释放其相关的资源。

  • 信号量在这个程序中确实起到了同步的作用,确保了同时只有一个线程可以进入临界区(信号量值为 1 时,只有一个线程可以继续,另一个线程必须等待)

具体的同步过程:

  • 通过使用信号量(sem_t sem),确保两个线程不会同时进入临界区(即 thread_func1thread_func2 中的代码块 printf("Inside critical section.\n");

  • 信号量通过阻塞和唤醒机制协调多个线程的执行顺序。例如,当 sem_wait(&sem) 在一个线程中被调用时,如果信号量的值为 0,该线程将被阻塞,直到另一个线程调用 sem_post(&sem) 释放信号量。

  • thread_func1thread_func2 开始执行时,它们都会调用 sem_wait(&sem)。此时,信号量的初始值为 1,表示资源(即进入临界区的权限)是可用的。

  • 假设 thread_func1 先调用 sem_wait(&sem),信号量值减为 0,thread_func1 进入临界区。thread_func2 此时如果调用 sem_wait(&sem),因为信号量值已经是 0,它将被阻塞,直到 thread_func1 调用 sem_post(&sem) 释放信号量。

  • thread_func1 退出临界区后调用 sem_post(&sem),信号量值重新变为 1,系统将唤醒 thread_func2,允许它进入临界区。

5. 进程的信号处理

进程的信号处理是指在操作系统中,进程对信号(Signal)的接收和响应方式。信号是一种异步的进程间通信方式,通常用于通知进程发生了某种事件,如终止、暂停、继续执行或捕获某些异常情况。进程可以通过定义信号处理程序来响应特定的信号,执行相应的处理逻辑。

5.1 常见信号及默认行为

信号 描述 默认行为
SIGINT 终端中断信号,用户按 Ctrl+C 触发 终止进程
SIGTERM 请求进程终止,允许进程进行清理工作 终止进程
SIGKILL 强制终止进程,无法被捕获或忽略 强制终止进程
SIGCHLD 子进程状态改变(如退出或停止) 忽略
SIGHUP 终端挂起或控制终端关闭时发送,通常用于通知守护进程重新加载配置文件 终止进程
SIGQUIT 从终端发出的退出信号(通常是 Ctrl+\),会生成核心转储 生成核心转储并终止进程
SIGILL 非法指令执行,如执行未定义的机器指令 生成核心转储并终止进程
SIGABRT 调用 abort() 函数引发的信号,表示进程异常终止 生成核心转储并终止进程
SIGFPE 算术运算错误(如除以零或溢出),浮点异常 生成核心转储并终止进程
SIGSEGV 段错误,非法内存访问 生成核心转储并终止进程
SIGPIPE 向无读者的管道写数据时引发,通常在管道通信中出现 终止进程
SIGBUS 总线错误,非法内存访问(如未对齐的内存访问) 生成核心转储并终止进程
SIGALRM alarm() 函数触发的定时信号 终止进程
SIGUSR1 用户自定义信号 1 终止进程(用户可定义处理行为)
SIGUSR2 用户自定义信号 2 终止进程(用户可定义处理行为)
SIGTRAP 断点陷阱,用于调试,通常由调试器捕获 生成核心转储并终止进程
SIGURG 紧急情况(out-of-band data)到达套接字 忽略
SIGXCPU 超出 CPU 时间限制 生成核心转储并终止进程
SIGXFSZ 超出文件大小限制 生成核心转储并终止进程
SIGVTALRM 虚拟计时器到期,通常用于跟踪用户时间消耗 终止进程
SIGPROF 计时器到期,通常用于统计进程的 CPU 使用情况 终止进程
SIGWINCH 终端窗口大小改变 忽略
SIGTSTP 终端停止信号,用户按 Ctrl+Z 触发,暂停进程 停止进程
SIGCONT 继续执行已停止的进程 继续执行(如果之前停止,则恢复执行)
SIGSTOP 停止进程,无法捕获或忽略 停止进程
SIGTTIN 后台进程组试图从终端读取数据时引发的信号 停止进程
SIGTTOU 后台进程组试图向终端写数据时引发的信号 停止进程

5.2 信号处理函数

函数 参数 参数描述 用途 描述
signal() signum 要处理的信号编号,如 SIGINT 注册简单的信号处理程序 用于指定一个信号处理函数,当进程接收到特定信号时执行。
handler 信号处理函数的指针,或特殊值 SIG_IGN(忽略信号)和 SIG_DFL(默认处理)
sigaction() signum 要处理的信号编号,如 SIGINT 注册信号处理程序,替代 signal sigactionsignal 的增强版本,允许更详细地控制信号处理行为。
act 包含新信号处理动作的 struct sigaction 结构的指针
oldact 保存先前信号处理设置的 struct sigaction 结构的指针
sigprocmask() how 指定如何修改信号屏蔽字的操作,如 SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK 改变信号屏蔽字,阻塞或解除阻塞信号 用于检查和更改进程的信号屏蔽字,可以阻塞或解除阻塞某些信号。
set 新的信号屏蔽字,表示要阻塞的信号
oldset 保存先前信号屏蔽字的指针
sigpending() set 存储当前挂起信号的信号集的指针 检查当前挂起的信号 检查进程当前挂起的信号集,即那些已经发送但因被阻塞而未处理的信号。
sigsuspend() mask 新的信号屏蔽字,用于临时替换当前信号屏蔽字 临时替换信号屏蔽字并挂起进程,等待特定信号 替换信号屏蔽字并挂起进程执行,直到接收到一个未被屏蔽的信号。
raise() sig 要发送的信号编号 向当前进程发送信号 用于在当前进程中发送信号,相当于在进程内部自发生成一个信号。
kill() pid 目标进程的进程 ID 向指定进程发送信号 向指定的进程发送信号,可以发送任何信号,而不仅仅是 SIGKILL
sig 要发送的信号编号
pause() 挂起进程执行,直到接收到一个信号 挂起进程的执行,直到收到并处理一个信号。

简单案例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

// 信号处理函数
void handle_signal(int sig) {
    switch (sig) {
        case SIGINT:
            printf("Caught SIGINT (Ctrl+C), but not terminating the process.\n");
            break;
        case SIGTERM:
            printf("Caught SIGTERM, terminating the process.\n");
            exit(0);
            break;
        case SIGCHLD:
            printf("Caught SIGCHLD, cleaning up child process.\n");
            wait(NULL);  // 清理子进程
            break;
        case SIGHUP:
            printf("Caught SIGHUP, reloading configuration...\n");
            // 模拟重新加载配置
            break;
        case SIGQUIT:
            printf("Caught SIGQUIT (Ctrl+\\), generating core dump.\n");
            abort();  // 生成核心转储并终止进程
            break;
        default:
            printf("Caught signal %d, no specific handler defined.\n", sig);
    }
}

int main() {
    // 使用 signal() 注册信号处理函数
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);
    signal(SIGCHLD, handle_signal);
    signal(SIGHUP, handle_signal);
    signal(SIGQUIT, handle_signal);

    // 创建一个子进程来演示 SIGCHLD
    if (fork() == 0) {
        printf("Child process started, will terminate in 5 seconds...\n");
        sleep(5);
        exit(0);
    }

    // 模拟守护进程的运行
    while (1) {
        printf("Running... (PID: %d)\n", getpid());
        sleep(2);  // 模拟长期运行的进程
    }

    return 0;
}

如何运行和测试:

  • Ctrl+C :触发 SIGINT 信号,处理函数会打印一条消息,但进程不会终止。
  • 发送 SIGTERM :可以通过命令 kill -SIGTERM <PID> 发送 SIGTERM 信号,处理函数会终止进程。
  • Ctrl+\ :触发 SIGQUIT 信号,处理函数会生成核心转储文件并终止进程。
  • 发送 SIGHUP :可以通过命令 kill -SIGHUP <PID> 发送 SIGHUP 信号,处理函数会模拟重新加载配置文件。
  • 等待子进程终止 :子进程终止时,父进程捕获 SIGCHLD 信号并清理子进程。

运行结果:

第一个终端:

Running... (PID: 1933357)

Child process started, will terminate in 5 seconds...

^CCaught SIGINT (Ctrl+C), but not terminating the process.

Caught SIGINT (Ctrl+C), but not terminating the process.

Running... (PID: 1933357)

Caught SIGCHLD, cleaning up child process.

Running... (PID: 1933357)

Caught SIGHUP, reloading configuration...

Running... (PID: 1933357)

Caught SIGTERM, terminating the process.

第二个终端:

liber@liber-VMware-Virtual-Platform:~$ kill -SIGHUP 1933357

liber@liber-VMware-Virtual-Platform:~$ kill -SIGTERM 1933357

6 常见的进程调度算法

6.1 先来先服务调度(FCFS, First-Come, First-Served)

先来先服务调度是一种最简单的调度算法。进程按照它们到达就绪队列的顺序依次运行,先到达的进程先执行,后到达的进程后执行。这个算法使用一个FIFO(First In, First Out)队列来管理进程顺序。

c 复制代码
#include <stdio.h>

// 定义进程结构体,用来保存每个进程的相关信息
struct Process {
    int pid;            // 进程ID
    int arrival_time;   // 到达时间
    int burst_time;     // 执行时间(运行所需时间)
    int completion_time;// 完成时间(进程完成执行的时间点)
    int waiting_time;   // 等待时间(进程在就绪队列中等待的时间)
    int turnaround_time;// 周转时间(从到达到完成所用的总时间)
};

// FCFS调度算法的实现
void fcfs_scheduling(struct Process processes[], int n) {
    int current_time = 0;  // 用于跟踪当前时间进度
    
    // 按照到达时间对进程排序,以确保先到的进程先执行
    for (int i = 0; i < n; i++) {
        // 如果当前时间小于进程的到达时间,CPU需要空闲等待
        if (current_time < processes[i].arrival_time) {
            current_time = processes[i].arrival_time;
        }
        // 计算进程的完成时间
        processes[i].completion_time = current_time + processes[i].burst_time;
        // 计算周转时间 = 完成时间 - 到达时间
        processes[i].turnaround_time = processes[i].completion_time - processes[i].arrival_time;
        // 计算等待时间 = 周转时间 - 执行时间
        processes[i].waiting_time = processes[i].turnaround_time - processes[i].burst_time;
        // 更新当前时间为此进程的完成时间
        current_time = processes[i].completion_time;
    }
}

// 打印所有进程的信息,包括PID、到达时间、执行时间、完成时间、等待时间、和周转时间
void print_processes(struct Process processes[], int n) {
    printf("PID\tArrival\tBurst\tCompletion\tWaiting\tTurnaround\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\t%d\t%d\t\t%d\t%d\n",
               processes[i].pid,
               processes[i].arrival_time,
               processes[i].burst_time,
               processes[i].completion_time,
               processes[i].waiting_time,
               processes[i].turnaround_time);
    }
}

int main() {
    int n;  // 进程数量

    // 用户输入进程的数量
    printf("输入进程数量: ");
    scanf("%d", &n);

    struct Process processes[n];  // 创建进程数组

    // 用户输入每个进程的到达时间和执行时间
    for (int i = 0; i < n; i++) {
        printf("输入进程 %d 的到达时间和执行时间: ", i + 1);
        scanf("%d %d", &processes[i].arrival_time, &processes[i].burst_time);
        processes[i].pid = i + 1;  // 进程ID从1开始
    }

    // 调用FCFS调度算法
    fcfs_scheduling(processes, n);
    // 打印调度结果
    print_processes(processes, n);

    return 0;
}

运行结果:

输入进程数量: 5

输入进程 1 的到达时间和执行时间: 1 1

输入进程 2 的到达时间和执行时间: 2 2

输入进程 3 的到达时间和执行时间: 3 3

输入进程 4 的到达时间和执行时间: 4 4

输入进程 5 的到达时间和执行时间: 5 5

PID Arrival Burst Completion Waiting Turnaround

1 1 1 2 0 1

2 2 2 4 0 2

3 3 3 7 1 4

4 4 4 11 3 7

5 5 5 16 6 11

6.2 短作业优先调度(SJF, Shortest Job First)

短作业优先调度是一种非抢占式调度算法。每次选择执行时间(即"作业长度")最短的进程进行调度。它可以降低平均等待时间,因为短作业可以尽快完成,减少后续作业的等待时间。

需要注意的是,SJF算法需要知道每个作业的执行时间(burst time),但在实际系统中,这通常是未知的,所以SJF在实际中主要作为理论模型或通过预测实现。

c 复制代码
#include <stdio.h>
#include <limits.h>  // 用于获取整型的最大值(INT_MAX)

// 定义进程结构体
struct Process {
    int pid;            // 进程ID
    int arrival_time;   // 到达时间
    int burst_time;     // 执行时间(运行所需时间)
    int completion_time;// 完成时间
    int waiting_time;   // 等待时间
    int turnaround_time;// 周转时间
    int is_completed;   // 标记进程是否已完成
};

// SJF调度算法的实现
void sjf_scheduling(struct Process processes[], int n) {
    int completed = 0;       // 已完成的进程数
    int current_time = 0;     // 当前时间

    // 当所有进程都未完成时,继续调度
    while (completed != n) {
        int shortest_index = -1;          // 保存最短作业的索引
        int shortest_burst = INT_MAX;     // 保存最短作业的执行时间,初始值设为最大整数

        // 寻找当前时间点上未完成的最短作业
        for (int i = 0; i < n; i++) {
            // 条件:进程已经到达、未完成、并且执行时间最短
            if (processes[i].arrival_time <= current_time && !processes[i].is_completed) {
                if (processes[i].burst_time < shortest_burst) {
                    shortest_burst = processes[i].burst_time;
                    shortest_index = i; // 记录该进程的索引
                }
            }
        }

        // 如果找到了合适的作业(即shortest_index有效)
        if (shortest_index != -1) {
            // 1. 将当前时间增加该短作业的执行时间
            current_time += processes[shortest_index].burst_time;

            // 2. 更新该进程的完成时间 = 当前时间
            processes[shortest_index].completion_time = current_time;

            // 3. 计算周转时间 = 完成时间 - 到达时间
            processes[shortest_index].turnaround_time = processes[shortest_index].completion_time - processes[shortest_index].arrival_time;

            // 4. 计算等待时间 = 周转时间 - 执行时间
            processes[shortest_index].waiting_time = processes[shortest_index].turnaround_time - processes[shortest_index].burst_time;

            // 5. 将该进程标记为已完成
            processes[shortest_index].is_completed = 1;

            // 6. 更新已完成的进程数
            completed++;
        } else {
            // 如果没有合适的作业可以运行,时间递增
            current_time++;
        }
    }
}

// 打印所有进程的信息,包括PID、到达时间、执行时间、完成时间、等待时间、和周转时间
void print_processes(struct Process processes[], int n) {
    printf("PID\tArrival\tBurst\tCompletion\tWaiting\tTurnaround\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\t%d\t%d\t\t%d\t%d\n",
               processes[i].pid,
               processes[i].arrival_time,
               processes[i].burst_time,
               processes[i].completion_time,
               processes[i].waiting_time,
               processes[i].turnaround_time);
    }
}

int main() {
    int n;  // 进程数量

    // 用户输入进程的数量
    printf("输入进程数量: ");
    scanf("%d", &n);

    struct Process processes[n];  // 创建进程数组

    // 用户输入每个进程的到达时间和执行时间
    for (int i = 0; i < n; i++) {
        printf("输入进程 %d 的到达时间和执行时间: ", i + 1);
        scanf("%d %d", &processes[i].arrival_time, &processes[i].burst_time);
        processes[i].pid = i + 1;          // 设置进程ID
        processes[i].is_completed = 0;     // 初始化进程为未完成状态
    }

    // 调用SJF调度算法
    sjf_scheduling(processes, n);
    
    // 打印调度结果
    print_processes(processes, n);

    return 0;
}

运行结果:

输入进程数量: 5

输入进程 1 的到达时间和执行时间: 1 2

输入进程 2 的到达时间和执行时间: 1 1

输入进程 3 的到达时间和执行时间: 1 4

输入进程 4 的到达时间和执行时间: 1 5

输入进程 5 的到达时间和执行时间: 1 3

PID Arrival Burst Completion Waiting Turnaround

1 1 2 4 1 3

2 1 1 2 0 1

3 1 4 11 6 10

4 1 5 16 10 15

5 1 3 7 3 6

6.3 最高优先级调度(Priority Scheduling)

最高优先级调度 是一种调度策略,进程根据其优先级进行调度,优先级越高的进程越早被调度。该算法可以是抢占式的(高优先级进程可以打断正在运行的低优先级进程)或非抢占式的(正在运行的进程不会被打断)。

在非抢占式优先级调度中,当前运行的进程会一直运行至完成,而在抢占式调度中,当一个高优先级的进程到达时,它会立即中断当前正在运行的低优先级进程。

非抢占式优先级调度:

c 复制代码
#include <stdio.h>

// 定义进程结构体
struct Process {
    int pid;            // 进程ID
    int arrival_time;   // 到达时间
    int burst_time;     // 执行时间(运行所需时间)
    int priority;       // 优先级
    int completion_time;// 完成时间
    int waiting_time;   // 等待时间
    int turnaround_time;// 周转时间
    int is_completed;   // 标记进程是否已完成
};

// 非抢占式优先级调度算法的实现
void priority_scheduling(struct Process processes[], int n) {
    int completed = 0;        // 已完成的进程数
    int current_time = 0;      // 当前时间

    // 当所有进程都未完成时,继续调度
    while (completed != n) {
        int highest_priority_index = -1;  // 保存最高优先级进程的索引
        int highest_priority = __INT_MAX__; // 用于比较优先级,初始为最大值(数值越低优先级越高)

        // 寻找当前时间点上未完成的最高优先级进程
        for (int i = 0; i < n; i++) {
            if (processes[i].arrival_time <= current_time && !processes[i].is_completed) {
                if (processes[i].priority < highest_priority) {
                    highest_priority = processes[i].priority;
                    highest_priority_index = i; // 记录该进程的索引
                }
            }
        }

        // 如果找到了合适的进程(即highest_priority_index有效)
        if (highest_priority_index != -1) {
            // 1. 将当前时间增加该进程的执行时间
            current_time += processes[highest_priority_index].burst_time;

            // 2. 更新该进程的完成时间 = 当前时间
            processes[highest_priority_index].completion_time = current_time;

            // 3. 计算周转时间 = 完成时间 - 到达时间
            processes[highest_priority_index].turnaround_time = processes[highest_priority_index].completion_time - processes[highest_priority_index].arrival_time;

            // 4. 计算等待时间 = 周转时间 - 执行时间
            processes[highest_priority_index].waiting_time = processes[highest_priority_index].turnaround_time - processes[highest_priority_index].burst_time;

            // 5. 将该进程标记为已完成
            processes[highest_priority_index].is_completed = 1;

            // 6. 更新已完成的进程数
            completed++;
        } else {
            // 如果没有合适的进程可以运行,时间递增
            current_time++;
        }
    }
}

// 打印所有进程的信息,包括PID、到达时间、执行时间、优先级、完成时间、等待时间和周转时间
void print_processes(struct Process processes[], int n) {
    printf("PID\tArrival\tBurst\tPriority\tCompletion\tWaiting\tTurnaround\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\t%d\t%d\t\t%d\t\t%d\t%d\n",
               processes[i].pid,
               processes[i].arrival_time,
               processes[i].burst_time,
               processes[i].priority,
               processes[i].completion_time,
               processes[i].waiting_time,
               processes[i].turnaround_time);
    }
}

int main() {
    int n;

    // 用户输入进程的数量
    printf("输入进程数量: ");
    scanf("%d", &n);

    struct Process processes[n];

    // 用户输入每个进程的到达时间、执行时间和优先级
    for (int i = 0; i < n; i++) {
        printf("输入进程 %d 的到达时间、执行时间和优先级: ", i + 1);
        scanf("%d %d %d", &processes[i].arrival_time, &processes[i].burst_time, &processes[i].priority);
        processes[i].pid = i + 1;          // 设置进程ID
        processes[i].is_completed = 0;     // 初始化进程为未完成状态
    }

    // 调用优先级调度算法
    priority_scheduling(processes, n);

    // 打印调度结果
    print_processes(processes, n);

    return 0;
}

运行结果:

输入进程数量: 3

输入进程 1 的到达时间、执行时间和优先级: 0 5 2

输入进程 2 的到达时间、执行时间和优先级: 1 3 1

输入进程 3 的到达时间、执行时间和优先级: 2 4 3

PID Arrival Burst Priority Completion Waiting Turnaround

1 0 5 2 5 0 5

2 1 3 1 8 4 7

3 2 4 3 12 6 10

说明:在非抢占式优先级调度中,当一个进程开始执行后,即使有更高(本代码数字越小,优先级越高)优先级的进程到达,也不会被打断,直到该进程完成。

抢占式优先级调度:

c 复制代码
#include <stdio.h>
#include <limits.h>

// 定义进程结构体
struct Process {
    int pid;            // 进程ID
    int arrival_time;   // 到达时间
    int burst_time;     // 执行时间(运行所需时间)
    int remaining_time; // 剩余执行时间
    int priority;       // 优先级
    int completion_time;// 完成时间
    int waiting_time;   // 等待时间
    int turnaround_time;// 周转时间
};

// 抢占式优先级调度算法的实现
void preemptive_priority_scheduling(struct Process processes[], int n) {
    int current_time = 0;       // 当前时间
    int completed = 0;          // 已完成的进程数

    while (completed != n) {
        int highest_priority_index = -1;
        int highest_priority = INT_MAX;

        // 寻找当前时间点上优先级最高且仍未完成的进程
        for (int i = 0; i < n; i++) {
            if (processes[i].arrival_time <= current_time && processes[i].remaining_time > 0) {
                if (processes[i].priority < highest_priority) {
                    highest_priority = processes[i].priority;
                    highest_priority_index = i;
                }
            }
        }

        // 如果找到了合适的进程
        if (highest_priority_index != -1) {
            // 运行该进程一个时间单位
            processes[highest_priority_index].remaining_time--;
            current_time++;

            // 如果进程完成
            if (processes[highest_priority_index].remaining_time == 0) {
                completed++;
                processes[highest_priority_index].completion_time = current_time;
                processes[highest_priority_index].turnaround_time = processes[highest_priority_index].completion_time - processes[highest_priority_index].arrival_time;
                processes[highest_priority_index].waiting_time = processes[highest_priority_index].turnaround_time - processes[highest_priority_index].burst_time;
            }
        } else {
            // 如果没有进程可以运行,时间递增
            current_time++;
        }
    }
}

// 打印所有进程的信息,包括PID、到达时间、执行时间、优先级、完成时间、等待时间和周转时间
void print_processes(struct Process processes[], int n) {
    printf("PID\tArrival\tBurst\tPriority\tCompletion\tWaiting\tTurnaround\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\t%d\t%d\t\t%d\t\t%d\t%d\n",
               processes[i].pid,
               processes[i].arrival_time,
               processes[i].burst_time,
               processes[i].priority,
               processes[i].completion_time,
               processes[i].waiting_time,
               processes[i].turnaround_time);
    }
}

int main() {
    int n;

    // 用户输入进程的数量
    printf("输入进程数量: ");
    scanf("%d", &n);

    struct Process processes[n];

    // 用户输入每个进程的到达时间、执行时间和优先级
    for (int i = 0; i < n; i++) {
        printf("输入进程 %d 的到达时间、执行时间和优先级: ", i + 1);
        scanf("%d %d %d", &processes[i].arrival_time, &processes[i].burst_time, &processes[i].priority);
        processes[i].pid = i + 1;                  // 设置进程ID
        processes[i].remaining_time = processes[i].burst_time; // 初始化剩余执行时间
    }

    // 调用抢占式优先级调度算法
    preemptive_priority_scheduling(processes, n);

    // 打印调度结果
    print_processes(processes, n);

    return 0;
}

运行结果:

输入进程数量: 3

输入进程 1 的到达时间、执行时间和优先级: 0 5 2

输入进程 2 的到达时间、执行时间和优先级: 1 3 1

输入进程 3 的到达时间、执行时间和优先级: 2 4 3

PID Arrival Burst Priority Completion Waiting Turnaround

1 0 5 2 8 3 8

2 1 3 1 4 0 3

3 2 4 3 12 6 10

说明:本代码优先级数字越小,优先级越高。

时间点 0:

  • 只有P1到达,P1开始执行,因为它是唯一的进程。

时间点 1:

  • P2到达,P2的优先级(1)高于正在执行的P1的优先级(2)。
  • 根据抢占式调度规则,P2立即抢占P1,P1被挂起。
  • P2开始执行。

时间点 4:

  • P2完成执行(它的执行时间是3个单位,从时间点1到4)。
  • P1继续执行,因为此时没有优先级更高的进程。

时间点 2:

  • P3到达,虽然它的优先级(3)低于P1(2),但此时P1已经被P2抢占,P3需要等待。
  • P1继续执行。

时间点 8:

  • P1完成执行,P3开始执行。

时间点 12:

  • P3完成执行。

6.4 轮转调度(Round Robin, RR)

**轮转调度(Round Robin, RR)**是一种时间片轮转调度算法。每个进程按照到达顺序进入队列,并按照固定的时间片(time quantum)运行。当一个进程的时间片用完时,它被放到队列的末尾,等待下一轮的调度。如果在时间片内进程未完成执行,它将在下一轮继续执行。轮转调度非常适合时间共享系统,它在处理器上公平地分配CPU时间,使得每个进程都有机会运行。

c 复制代码
#include <stdio.h>

// 定义进程结构体
struct Process {
    int pid;            // 进程ID
    int arrival_time;   // 到达时间
    int burst_time;     // 执行时间
    int remaining_time; // 剩余执行时间
    int completion_time;// 完成时间
    int waiting_time;   // 等待时间
    int turnaround_time;// 周转时间
};

// 轮转调度算法的实现
void round_robin_scheduling(struct Process processes[], int n, int time_quantum) {
    int current_time = 0;  // 当前时间
    int completed = 0;     // 已完成的进程数
    int i = 0;             // 进程索引

    // 循环直到所有进程完成
    while (completed != n) {
        // 如果当前进程已到达且仍有剩余时间需要执行
        if (processes[i].arrival_time <= current_time && processes[i].remaining_time > 0) {
            // 如果剩余时间大于时间片,则运行时间片大小的时间
            if (processes[i].remaining_time > time_quantum) {
                current_time += time_quantum;                // 增加当前时间
                processes[i].remaining_time -= time_quantum; // 减少剩余执行时间
            } else {
                // 否则运行剩余时间并完成进程
                current_time += processes[i].remaining_time; // 增加当前时间
                processes[i].remaining_time = 0;             // 设置剩余执行时间为0
                processes[i].completion_time = current_time; // 更新完成时间

                // 计算周转时间 = 完成时间 - 到达时间
                processes[i].turnaround_time = processes[i].completion_time - processes[i].arrival_time;

                // 计算等待时间 = 周转时间 - 执行时间
                processes[i].waiting_time = processes[i].turnaround_time - processes[i].burst_time;

                completed++; // 进程已完成,增加已完成的进程数
            }
        }

        // 循环遍历下一个进程
        i = (i + 1) % n;
    }
}

// 打印所有进程的信息,包括PID、到达时间、执行时间、完成时间、等待时间和周转时间
void print_processes(struct Process processes[], int n) {
    printf("PID\tArrival\tBurst\tCompletion\tWaiting\tTurnaround\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\t%d\t%d\t\t%d\t%d\n",
               processes[i].pid,
               processes[i].arrival_time,
               processes[i].burst_time,
               processes[i].completion_time,
               processes[i].waiting_time,
               processes[i].turnaround_time);
    }
}

int main() {
    int n, time_quantum;

    // 用户输入进程数量
    printf("输入进程数量: ");
    scanf("%d", &n);

    struct Process processes[n];

    // 用户输入每个进程的到达时间和执行时间
    for (int i = 0; i < n; i++) {
        printf("输入进程 %d 的到达时间和执行时间: ", i + 1);
        scanf("%d %d", &processes[i].arrival_time, &processes[i].burst_time);
        processes[i].pid = i + 1;                            // 设置进程ID
        processes[i].remaining_time = processes[i].burst_time; // 初始化剩余执行时间为执行时间
    }

    // 用户输入时间片大小
    printf("输入时间片大小: ");
    scanf("%d", &time_quantum);

    // 调用轮转调度算法
    round_robin_scheduling(processes, n, time_quantum);

    // 打印调度结果
    print_processes(processes, n);

    return 0;
}

运行结果:

输入进程数量: 3

输入进程 1 的到达时间和执行时间: 0 5

输入进程 2 的到达时间和执行时间: 1 3

输入进程 3 的到达时间和执行时间: 2 4

输入时间片大小: 2

PID Arrival Burst Completion Waiting Turnaround

1 0 5 12 7 12

2 1 3 9 5 8

3 2 4 11 5 9

时间点 0-2

  • P1到达并执行2个时间单位(时间片大小为2),剩余执行时间为3。
  • 当前时间为2。

时间点 2-4

  • P2到达(时间点1),开始执行2个时间单位,剩余执行时间为1。
  • 当前时间为4。

时间点 4-6

  • P3到达(时间点2),开始执行2个时间单位,剩余执行时间为2。
  • 当前时间为6。

时间点 6-8

  • P1轮到再次执行,执行2个时间单位,剩余1个时间单位。
  • 当前时间为8。

时间点 8-9

  • P2轮到继续执行,执行1个时间单位,完成任务。
  • 当前时间为9。

时间点 9-10

  • P3继续执行,执行1个时间单位,剩余1个时间单位。
  • 当前时间为10。

时间点 10-11

  • P3最后执行剩余的1个时间单位,完成任务。
  • 当前时间为11。

时间点 11-12

  • P1最后执行剩余的1个时间单位,完成任务。
  • 当前时间为12。

6.5 设置进程优先级

设置进程优先级 是指在操作系统中为每个进程分配一个优先级值,该值决定了进程在调度过程中的优先顺序。优先级越高的进程通常会比优先级低的进程更早获得CPU资源,这对于实时操作系统和任务管理至关重要。在Linux中,使用nice命令可以设置进程的优先级。nice值的范围通常是从-20(最高优先级)到19(最低优先级),默认值为0

命令示例:

sh 复制代码
# 以更高优先级启动程序
nice -n -10 ./my_program

# 调整正在运行的进程优先级
renice -n 5 -p 1234  # 将 PID 为 1234 的进程优先级降低

# 查看
top

运行结果:

第一个终端:

liber@liber-VMware-Virtual-Platform:/home/c$ nice -n 10 ./receiver

第二个终端:

top - 16:47:25 up 1 day, 22:26, 9 users, load average: 2.84, 2.60, 2.17

任务: 355 total, 2 running, 353 sleeping, 0 stopped, 0 zombie

%Cpu(s): 13.0 us, 24.8 sy, 41.1 ni, 19.6 id, 0.0 wa, 0.0 hi, 1.4 si, 0.0 st

MiB Mem : 3868.2 total, 122.3 free, 2645.8 used, 1346.7 buff/cache

MiB Swap: 3868.0 total, 2832.5 free, 1035.5 used. 1222.5 avail Mem

进程号 USER PR NI VIRT RES SHR %CPU %MEM TIME+ COMMAND

2100616 liber 30 10 2680 1408 1408 R 77.9 0.0 0:46.70 receiver

说明:top命令是一个动态显示当前系统任务的命令,它可以显示每个进程的PR(优先级)和NI(nice值)。

第三个终端:

liber@liber-VMware-Virtual-Platform:~$ sudo renice -n 5 -p 2100616

[sudo] liber 的密码:

2100616 (process ID) 旧优先级为 10,新优先级为 5

7. 进程的内存管理

7.1 地址空间

  • 物理地址:这是计算机内存芯片上的实际地址,由内存硬件直接使用。
  • 逻辑地址(虚拟地址):这是由CPU生成的地址,是相对与进程独立的虚拟地址空间的地址,通常通过地址转换机制映射到物理地址。

7.2 内存分配

  • 静态分配:在程序编译时为变量分配固定的内存空间,无法在运行时调整。
  • 动态分配:在程序运行时,根据需要分配和释放内存,例如使用堆(heap)和栈(stack)进行管理。

7.3 常见的机制

机制 描述
分段 内存被划分为不同的段,每个段可以包含不同类型的数据,如代码段、数据段、栈段等。每个段有自己的基址和长度。分段使得程序员可以将逻辑相关的内存部分分开处理,有助于保护和共享内存。
分页 内存被划分为固定大小的页(通常是4KB)。虚拟内存的每一页可以映射到物理内存的任意一页。分页减少了内存碎片的产生,使内存管理更加高效。
虚拟内存 虚拟内存是一种内存管理技术,它允许操作系统使用磁盘空间来扩展物理内存的容量。通过将不常用的页面交换到磁盘,虚拟内存让系统能够运行超出实际物理内存容量的程序。
页面置换算法 当物理内存满了,需要将某些页面换出到磁盘,这时会使用页面置换算法。

7.4 页面置换算法

7.4.1 FIFO(First-In, First-Out)页面置换算法

FIFO页面置换算法基于"先来先服务"的原则。它将最早进入内存的页面作为最先被置换的候选者,不考虑页面的使用频率或最近使用时间。当需要置换页面时,系统会选择最早进入内存的页面,将其换出,然后将新页面加载到该位置。使用队列(Queue)来跟踪页面的顺序。队列的头部是最早进入的页面,尾部是最新进入的页面。当新页面进入时,移除队列头部的页面,将新页面添加到尾部。

简单案例:

c 复制代码
#include <stdio.h>

#define MAX_FRAMES 3 // 最大页框数

void fifo_page_replacement(int pages[], int n) {
    int frames[MAX_FRAMES] = {-1, -1, -1}; // 初始化页框为空,-1 表示空闲
    int current = 0; // 当前页框的索引
    int page_faults = 0; // 记录页面错误次数

    printf("FIFO Page Replacement\n");
    printf("Page\tFrames\n");

    for (int i = 0; i < n; i++) {
        int page = pages[i];
        int found = 0;

        // 检查页面是否已经在页框中
        for (int j = 0; j < MAX_FRAMES; j++) {
            if (frames[j] == page) {
                found = 1; // 页面已在内存中,不需要替换
                break;
            }
        }

        if (!found) {
            // 如果页面不在内存中,替换最早进入的页面
            frames[current] = page; // 替换当前页框中的页面
            current = (current + 1) % MAX_FRAMES; // 更新索引,确保循环替换
            page_faults++;

            // 打印当前页框的内容
            printf("%d\t", page);
            for (int j = 0; j < MAX_FRAMES; j++) {
                if (frames[j] != -1) {
                    printf("%d ", frames[j]); // 打印页框中的页面
                } else {
                    printf("- "); // 打印空闲页框
                }
            }
            printf("\n");
        }
    }

    printf("Total Page Faults: %d\n", page_faults); // 打印总的页面错误次数
}

int main() {
    int n;

    // 输入页面序列长度
    printf("输入页面序列长度: ");
    scanf("%d", &n);

    int pages[n];

    // 输入页面序列
    printf("输入页面序列(以空格分隔): ");
    for (int i = 0; i < n; i++) {
        scanf("%d", &pages[i]);
    }

    fifo_page_replacement(pages, n);

    return 0;
}

运行结果:

输入页面序列长度: 6

输入页面序列(以空格分隔): 1 3 0 5 6 3

FIFO Page Replacement

Page Frames

1 1 - -

3 1 3 -

0 1 3 0

5 5 3 0

6 5 6 0

3 5 6 3

Total Page Faults: 6

解释:

  • 页面1 加载到内存,产生页面错误,内存状态:1 - -

  • 页面3 加载到内存,产生页面错误,内存状态:1 3 -

  • 页面0 加载到内存,产生页面错误,内存状态:1 3 0

  • 页面5 加载到内存,替换1(最早进入),产生页面错误,内存状态:5 3 0

  • 页面6 加载到内存,替换3(最早进入),产生页面错误,内存状态:5 6 0

  • 页面3 加载到内存,替换0,产生页面错误,内存状态:5 6 3

7.4.2 LRU(Least Recently Used)页面置换算法

LRU页面置换算法基于"最近最少使用"原则,选择最久未被访问的页面进行置换。系统需要记录每个页面的最后一次访问时间或访问顺序。当需要置换页面时,选择记录中访问时间最久远的页面进行置换。

简单案例:

c 复制代码
#include <stdio.h>

#define MAX_FRAMES 3 // 最大页框数

void lru_page_replacement(int pages[], int n) {
    int frames[MAX_FRAMES] = {-1, -1, -1}; // 初始化页框为空,-1 表示空闲
    int counter[MAX_FRAMES] = {0}; // 记录每个帧的使用时间
    int page_faults = 0;
    int time = 0; // 用于记录当前的时间

    printf("LRU Page Replacement\n");
    printf("Page\tFrames\n");

    for (int i = 0; i < n; i++) {
        int page = pages[i];
        int found = 0;

        // 检查页面是否已经在页框中
        for (int j = 0; j < MAX_FRAMES; j++) {
            if (frames[j] == page) {
                found = 1; // 页面已在内存中,不需要替换
                counter[j] = ++time; // 更新此页面的使用时间
                break;
            }
        }

        if (!found) {
            // 查找最近最少使用的帧
            int lru_index = 0;
            for (int j = 1; j < MAX_FRAMES; j++) {
                if (counter[j] < counter[lru_index]) {
                    lru_index = j; // 选择最近最少使用的页面进行替换
                }
            }

            // 替换最久未使用的页面
            frames[lru_index] = page;
            counter[lru_index] = ++time; // 更新使用时间
            page_faults++;

            // 打印当前页框的内容
            printf("%d\t", page);
            for (int j = 0; j < MAX_FRAMES; j++) {
                if (frames[j] != -1) {
                    printf("%d ", frames[j]); // 打印页框中的页面
                } else {
                    printf("- "); // 打印空闲页框
                }
            }
            printf("\n");
        }
    }

    printf("Total Page Faults: %d\n", page_faults); // 打印总的页面错误次数
}

int main() {
    int n;

    // 输入页面序列长度
    printf("输入页面序列长度: ");
    scanf("%d", &n);

    int pages[n];

    // 输入页面序列
    printf("输入页面序列(以空格分隔): ");
    for (int i = 0; i < n; i++) {
        scanf("%d", &pages[i]);
    }

    // 调用LRU页面置换算法
    lru_page_replacement(pages, n);

    return 0;
}

运行结果:

输入页面序列长度: 6

输入页面序列(以空格分隔): 1 3 0 3 5 6

LRU Page Replacement

Page Frames

1 1 - -

3 1 3 -

0 1 3 0

5 5 3 0

6 5 3 6

Total Page Faults: 5

解释:

  • 页面1 加载到内存,产生页面错误,内存状态:1 - -

  • 页面3 加载到内存,产生页面错误,内存状态:1 3 -

  • 页面0 加载到内存,产生页面错误,内存状态:1 3 0

  • 页面3已经在内存中,更新其使用时间,无需替换,不产生页面错误。

  • 页面5 加载到内存,替换最近最少使用的页面1,产生页面错误,内存状态:5 3 0

  • 页面6 加载到内存,替换最近最少使用的页面0,产生页面错误,内存状态:5 3 6

7.4.3 LFU(Least Frequently Used)页面置换算法

LFU页面置换算法基于"最少使用频率"原则,选择在一段时间内使用次数最少的页面进行置换。为每个页面维护一个计数器,每次访问该页面时,计数器加1。当需要置换页面时,选择计数器值最小的页面。

简单案例:

c 复制代码
#include <stdio.h>

#define MAX_FRAMES 3 // 最大页框数

void lfu_page_replacement(int pages[], int n) {
    int frames[MAX_FRAMES] = {-1, -1, -1}; // 初始化页框为空,-1 表示空闲
    int frequency[MAX_FRAMES] = {0}; // 记录每个帧的使用频率
    int age[MAX_FRAMES] = {0}; // 记录每个帧的使用顺序,用于解决频率相同时的选择
    int page_faults = 0;
    int time = 0; // 用于记录每次页面访问的时间

    printf("LFU Page Replacement\n");
    printf("Page\tFrames\n");

    for (int i = 0; i < n; i++) {
        int page = pages[i];
        int found = 0;

        // 检查页面是否已经在页框中
        for (int j = 0; j < MAX_FRAMES; j++) {
            if (frames[j] == page) {
                found = 1; // 页面已在内存中
                frequency[j]++; // 增加使用频率
                age[j] = ++time; // 更新此页面的访问时间
                break;
            }
        }

        if (!found) {
            // 选择使用频率最低且最早进入的页面进行替换
            int lfu_index = 0;
            for (int j = 1; j < MAX_FRAMES; j++) {
                // 比较频率,频率低的优先
                if (frequency[j] < frequency[lfu_index]) {
                    lfu_index = j;
                } 
                // 如果频率相同,选择最早进入的页面
                else if (frequency[j] == frequency[lfu_index] && age[j] < age[lfu_index]) {
                    lfu_index = j;
                }
            }

            // 替换使用频率最低的页面
            frames[lfu_index] = page;
            frequency[lfu_index] = 1; // 重置使用频率
            age[lfu_index] = ++time; // 更新页面进入时间
            page_faults++;
        }

        // 无论页面是否已经在内存中,都打印当前页框的内容
        printf("%d\t", page);
        for (int j = 0; j < MAX_FRAMES; j++) {
            if (frames[j] != -1) {
                printf("%d ", frames[j]); // 打印页框中的页面
            } else {
                printf("- "); // 打印空闲页框
            }
        }
        printf("\n");
    }

    printf("Total Page Faults: %d\n", page_faults); // 打印总的页面错误次数
}

int main() {
    int n;

    // 输入页面序列长度
    printf("输入页面序列长度: ");
    scanf("%d", &n);

    int pages[n];

    // 输入页面序列
    printf("输入页面序列(以空格分隔): ");
    for (int i = 0; i < n; i++) {
        scanf("%d", &pages[i]);
    }

    // 调用 LFU 页面置换算法
    lfu_page_replacement(pages, n);

    return 0;
}

运行结果:

输入页面序列长度: 8

输入页面序列(以空格分隔): 1 2 3 4 2 1 5 6

LFU Page Replacement

Page Frames

1 1 - -

2 1 2 -

3 1 2 3

4 4 2 3

2 4 2 3

1 4 2 1

5 5 2 1

6 5 2 6

Total Page Faults: 7

解释:

  • 页面1 :加载到内存,产生页面错误,内存状态:1 - -

  • 页面2 :加载到内存,产生页面错误,内存状态:1 2 -

  • 页面3 :加载到内存,产生页面错误,内存状态:1 2 3

  • 页面4 :在频率相同的情况下,应该选择最早的页面1替换,产生页面错误,内存状态:4 2 3

  • 页面2:已在内存中,增加其频率,无需替换。

  • 页面1 :在频率相同的情况下,应该选择最早的页面3替换,产生页面错误,内存状态:4 2 1

  • 页面5 :在频率相同的情况下,应该选择最早的页面4替换,产生页面错误,内存状态:5 2 1

  • 页面6 :在频率相同的情况下,应该选择最早的页面1替换,产生页面错误,内存状态:5 2 6

7.5 内存映射

内存映射(Memory Mapping)是操作系统提供的一种机制,它允许将文件或其他对象(如设备内存)直接映射到进程的地址空间。通过内存映射,进程可以像访问普通内存一样访问文件中的数据,而不需要使用传统的I/O操作。

mmap() 是一种高效的内存管理技术,可以将文件或设备映射到进程的地址空间,允许进程通过内存直接访问文件内容。

简单案例:

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    // 打开要映射的文件
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 获取文件大小
    struct stat st;
    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }

    // 将文件内容映射到内存
    char *mapped = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd)
        return 1;
    }

    // 关闭文件描述符
    close(fd);

    // 现在可以直接访问文件内容
    printf("File content:\n%.*s\n", (int)st.st_size, mapped);

    // 解除内存映射
    if (munmap(mapped, st.st_size) == -1) {
        perror("munmap");
        return 1;
    }

    return 0;
}

运行结果:

前提准备:创建example.txt,随便输入一些内容,我输入的是HI,Word!

File content:

HI,Word!

解释

  • 使用 mmap 将文件内容映射到进程的地址空间。PROT_READ 表示映射区域是可读的,MAP_PRIVATE 表示创建私有副本,修改映射内容不会影响原文件。
  • 映射成功后,可以通过指针 mapped 直接访问文件内容。
  • 在不需要时,使用 munmap 解除内存映射,释放资源。

8. 进程的同步与锁机制

进程同步 是指在多个进程或线程并发执行时,控制它们的执行顺序或访问顺序,以确保共享资源被正确使用。进程同步的目标是避免以下问题:

  • 竞争条件:多个进程或线程同时访问或修改共享资源,可能导致数据不一致或不可预知的行为。
  • 死锁:两个或多个进程或线程相互等待对方释放资源,导致所有进程或线程都无法继续执行。
  • 资源饥饿:某些进程或线程长期无法获得资源,导致无法正常执行。

锁机制 是实现进程同步的常用手段之一。锁可以用来确保在某一时刻,只有一个进程或线程能够访问特定的共享资源

进程同步与锁机制的主要类型及其特点:

机制类型 描述 操作 适用场景
互斥锁 (Mutex) 确保同一时刻只有一个进程或线程可以访问共享资源,避免数据竞争。 Lock:请求锁,锁被占用时阻塞。 Unlock:释放锁。 需要独占访问资源的场景,如更新共享数据结构。
读写锁 (Read-Write Lock) 允许多个读者同时读取,但写者独占访问,适用于读多写少的情况。 Read Lock:请求读锁,可并发。 Write Lock:请求写锁,独占。 Unlock:释放锁。 读多写少的资源访问,如缓存。
自旋锁 (Spinlock) 进程或线程忙等待,持续检查锁状态,适用于锁定时间很短的情况。 Lock:忙等待获取锁。 Unlock:释放锁。 短时间锁定的临界区,避免上下文切换的开销。
信号量 (Semaphore) 用于限制同时访问共享资源的进程或线程的数量,控制并发。 P (Wait):请求减少信号量值,阻塞等待。 V (Signal):增加信号量值,可能唤醒等待进程。 控制资源访问数量,如限制同时访问数据库连接数。
条件变量 (Condition Variable) 用于进程或线程间的信号传递,等待某一条件发生时进行同步。 Wait:等待条件满足。 Signal:条件满足时发信号唤醒。 需要在特定条件下进行线程或进程间的同步,如生产者-消费者模型。
屏障 (Barrier) 使一组线程或进程必须全部到达某个同步点后才能继续执行,常用于并行计算。 Wait:等待所有线程或进程到达屏障点。 并行计算中各部分任务需要同步协调的场景,如阶段性任务同步。

8.1 互斥锁 (Mutex)

互斥锁是用于防止多个线程或进程同时访问共享资源的锁机制。互斥锁保证在任一时刻,只有一个线程可以持有锁并访问受保护的共享资源。其他线程在尝试获取已被持有的互斥锁时,会被阻塞,直到锁被释放。

  • 锁定(Lock):线程请求互斥锁。如果锁已经被其他线程持有,请求线程会被阻塞,直到锁可用。
  • 解锁(Unlock):线程释放互斥锁,使得其他阻塞的线程可以继续执行。
c 复制代码
#include <stdio.h>
#include <pthread.h>

// 定义互斥锁
pthread_mutex_t lock;

void *thread_function(void *arg) {
    int id = *(int *)arg;

    // 尝试获取互斥锁
    pthread_mutex_lock(&lock);

    // 进入临界区,访问共享资源
    printf("Thread %d is executing\n", id);

    // 模拟临界区操作
    for (int i = 0; i < 5; i++) {
        printf("Thread %d is in critical section %d\n", id, i+1);
    }

    // 释放互斥锁
    pthread_mutex_unlock(&lock);

    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;

    // 初始化互斥锁
    pthread_mutex_init(&lock, NULL);

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, &id1);
    pthread_create(&thread2, NULL, thread_function, &id2);

    // 等待两个线程完成
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 销毁互斥锁
    pthread_mutex_destroy(&lock);

    return 0;
}

运行结果:

Thread 1 is executing

Thread 1 is in critical section 1

Thread 1 is in critical section 2

Thread 1 is in critical section 3

Thread 1 is in critical section 4

Thread 1 is in critical section 5

Thread 2 is executing

Thread 2 is in critical section 1

Thread 2 is in critical section 2

Thread 2 is in critical section 3

Thread 2 is in critical section 4

Thread 2 is in critical section 5

解释:

  • pthread_mutex_init(&lock, NULL); 初始化互斥锁。在使用互斥锁前,必须先初始化它。

  • 每个线程在进入临界区前都会尝试获取互斥锁。使用 pthread_mutex_lock(&lock); 来获取锁。如果锁已经被其他线程持有,当前线程会被阻塞,直到锁可用。

  • 进入临界区后,线程可以安全地访问共享资源,如打印消息或修改共享变量。

  • 完成临界区操作后,使用 pthread_mutex_unlock(&lock); 释放锁,使得其他线程可以继续执行。

  • (线程同步)pthread_join(thread1, NULL);pthread_join(thread2, NULL); 用于等待两个线程完成执行,确保主线程在它们执行完毕后才继续。

  • pthread_mutex_destroy(&lock); 在程序结束时销毁互斥锁,释放资源。

8.2 读写锁 (Read-Write Lock)

读写锁 (Read-Write Lock) 是一种允许多个线程同时读取共享资源,但只允许一个线程写入共享资源的锁机制。读写锁提供了更高的并发性,因为它允许多线程并发读取数据,同时防止写操作和其他读写操作的冲突。

  • 读锁(Read Lock):多个线程可以同时获取读锁,允许并发读取共享资源。
  • 写锁(Write Lock):获取写锁时,必须等待所有读锁和其他写锁被释放,确保写操作的独占性。
  • 解锁(Unlock):释放读锁或写锁,允许其他等待的线程继续执行。
c 复制代码
#include <stdio.h>
#include <pthread.h>

// 定义读写锁
pthread_rwlock_t rwlock;

// 共享资源
int shared_resource = 0;

void *reader(void *arg) {
    int id = *(int *)arg;

    // 获取读锁
    pthread_rwlock_rdlock(&rwlock);

    // 读取共享资源
    printf("Reader %d: Shared resource = %d\n", id, shared_resource);

    // 释放读锁
    pthread_rwlock_unlock(&rwlock);

    return NULL;
}

void *writer(void *arg) {
    int id = *(int *)arg;

    // 获取写锁
    pthread_rwlock_wrlock(&rwlock);

    // 写入共享资源
    shared_resource += 10;
    printf("Writer %d: Updated shared resource to %d\n", id, shared_resource);

    // 释放写锁
    pthread_rwlock_unlock(&rwlock);

    return NULL;
}

int main() {
    pthread_t threads[3];
    int ids[3] = {1, 2, 3};

    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 创建两个读线程和一个写线程
    pthread_create(&threads[0], NULL, reader, &ids[0]);
    pthread_create(&threads[1], NULL, writer, &ids[1]);
    pthread_create(&threads[2], NULL, reader, &ids[2]);

    // 等待所有线程完成
    for (int i = 0; i < 3; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

运行结果:

Reader 1: Shared resource = 0

Writer 2: Updated shared resource to 10

Reader 3: Shared resource = 10

解释:

  • pthread_rwlock_init(&rwlock, NULL); 初始化读写锁。在使用读写锁前,必须先初始化它。

  • 每个读线程在读取共享资源前会获取读锁。使用 pthread_rwlock_rdlock(&rwlock); 来获取读锁。多个读线程可以同时获取读锁,允许并发读取。

  • 读取共享资源后,使用 pthread_rwlock_unlock(&rwlock); 释放读锁。

  • 写线程在修改共享资源前会获取写锁。使用 pthread_rwlock_wrlock(&rwlock); 来获取写锁。获取写锁后,写线程可以独占访问共享资源。

  • 修改共享资源后,使用 pthread_rwlock_unlock(&rwlock); 释放写锁,允许其他读写操作继续执行。

  • pthread_join(threads[i], NULL); 用于等待所有线程完成执行,确保主线程在它们执行完毕后才继续。

  • pthread_rwlock_destroy(&rwlock); 在程序结束时销毁读写锁,释放资源。

8.3 自旋锁 (Spinlock)

自旋锁 (Spinlock) 是一种忙等待的锁机制。在等待获取锁时,线程不会进入睡眠状态,而是持续检查锁的状态,直到获得锁为止。由于自旋锁不会导致线程上下文切换,因此适用于锁定时间非常短的场景。

  • 锁定(Lock):线程尝试获取自旋锁。如果锁已被占用,线程会在获取锁之前一直循环检查锁的状态。
  • 解锁(Unlock):线程释放自旋锁,允许其他等待的线程获取锁。
c 复制代码
#include <stdio.h>
#include <pthread.h>

// 定义自旋锁
pthread_spinlock_t spinlock;

// 共享资源
int shared_resource = 0;

void *thread_function(void *arg) {
    int id = *(int *)arg;

    // 尝试获取自旋锁
    pthread_spin_lock(&spinlock);

    // 进入临界区,访问共享资源
    printf("Thread %d is executing\n", id);
    shared_resource += 10;
    printf("Thread %d updated shared resource to %d\n", id, shared_resource);

    // 释放自旋锁
    pthread_spin_unlock(&spinlock);

    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;

    // 初始化自旋锁
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, &id1);
    pthread_create(&thread2, NULL, thread_function, &id2);

    // 等待两个线程完成
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 销毁自旋锁
    pthread_spin_destroy(&spinlock);

    return 0;
}

运行结果:

liber@liber-VMware-Virtual-Platform:/home/c$ ./receiver

Thread 2 is executing

Thread 2 updated shared resource to 10

Thread 1 is executing

Thread 1 updated shared resource to 20

解释:

  • pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE); 初始化自旋锁。在使用自旋锁前,必须先初始化它。这里的 PTHREAD_PROCESS_PRIVATE 表示自旋锁仅用于单个进程内的线程之间。

  • 每个线程在进入临界区前都会尝试获取自旋锁。使用 pthread_spin_lock(&spinlock); 来获取锁。自旋锁不会让线程进入睡眠,而是会忙等待直到锁可用。

  • 进入临界区后,线程可以安全地访问共享资源,如增加共享变量的值。

  • 完成临界区操作后,使用 pthread_spin_unlock(&spinlock); 释放锁,使得其他线程可以继续执行。

  • pthread_join(thread1, NULL);pthread_join(thread2, NULL); 用于等待两个线程完成执行,确保主线程在它们执行完毕后才继续。

  • pthread_spin_destroy(&spinlock); 在程序结束时销毁自旋锁,释放资源。

8.4 信号量 (Semaphore)

信号量 (Semaphore) 是一种用于控制多个进程或线程对共享资源进行访问的同步机制。信号量包含一个计数器,用来表示当前可用资源的数量。信号量可以用来解决资源的互斥访问问题,特别是在需要限制访问资源的进程或线程数量时。

  • P 操作 (Wait/Down):试图将信号量的值减1,如果信号量的值为0,则进程或线程会被阻塞,直到信号量的值大于0。
  • V 操作 (Signal/Up):将信号量的值加1,如果有等待的进程或线程,则唤醒它们。
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

// 定义信号量
sem_t semaphore;

// 共享资源
int shared_resource = 0;

void *thread_function(void *arg) {
    int id = *(int *)arg;

    // 尝试获取信号量
    sem_wait(&semaphore); // P 操作,等待信号量

    // 进入临界区,访问共享资源
    printf("Thread %d is executing\n", id);
    shared_resource += 10;
    printf("Thread %d updated shared resource to %d\n", id, shared_resource);

    // 释放信号量
    sem_post(&semaphore); // V 操作,释放信号量

    return NULL;
}

int main() {
    pthread_t threads[3];
    int ids[3] = {1, 2, 3};

    // 初始化信号量,初始值为1,表示同一时刻只能有一个线程进入临界区
    sem_init(&semaphore, 0, 1);

    // 创建三个线程
    for (int i = 0; i < 3; i++) {
        pthread_create(&threads[i], NULL, thread_function, &ids[i]);
    }

    // 等待所有线程完成
    for (int i = 0; i < 3; i++) {
        pthread_join(threads[i], NULL);
    }

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

    return 0;
}

运行结果(其中一个):

Thread 1 is executing

Thread 1 updated shared resource to 10

Thread 2 is executing

Thread 2 updated shared resource to 20

Thread 3 is executing

Thread 3 updated shared resource to 30

解释:

  • sem_init(&semaphore, 0, 1); 初始化信号量。0表示信号量只在当前进程内有效,1是信号量的初始值,表示同时只能有一个线程访问资源。

  • 每个线程在进入临界区前会执行 sem_wait(&semaphore); 进行 P 操作(Wait/Down)。如果信号量的值为正,则将其减1并进入临界区。如果信号量的值为0,线程将被阻塞,直到信号量的值大于0。

  • 在临界区内,线程可以安全地访问和修改共享资源。

  • 完成操作后,使用 sem_post(&semaphore); 进行 V 操作(Signal/Up),将信号量的值加1,允许其他被阻塞的线程继续执行。

  • pthread_join(threads[i], NULL); 用于等待所有线程完成执行,确保主线程在它们执行完毕后才继续。

  • sem_destroy(&semaphore); 在程序结束时销毁信号量,释放资源。

8.5 条件变量 (Condition Variable)

条件变量 (Condition Variable) 是一种允许线程等待某个条件发生并在条件满足时进行通知的同步机制。条件变量通常与互斥锁一起使用,以确保在检查或修改共享数据时线程不会进入竞争条件。

  • 等待 (Wait):线程在条件变量上等待,等待期间会释放与之关联的互斥锁,直到被唤醒。当被唤醒时,线程重新获取互斥锁并继续执行。
  • 通知 (Signal/Broadcast) :唤醒等待条件变量的一个或所有线程。Signal 通知一个线程,Broadcast 通知所有等待线程。
c 复制代码
#include <stdio.h>
#include <pthread.h>

#define BUFFER_SIZE 5

// 共享资源
int buffer[BUFFER_SIZE];
int count = 0;

// 互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t cond_producer;
pthread_cond_t cond_consumer;

void *producer(void *arg) {
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);

        // 如果缓冲区满,等待消费者消费
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex);
        }

        // 生产数据
        buffer[count] = i;
        printf("Produced: %d\n", buffer[count]);
        count++;

        // 通知消费者
        pthread_cond_signal(&cond_consumer);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void *consumer(void *arg) {
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);

        // 如果缓冲区空,等待生产者生产
        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex);
        }

        // 消费数据
        printf("Consumed: %d\n", buffer[0]);

        // 将缓冲区中的数据前移,保持顺序
        for (int j = 0; j < count - 1; j++) {
            buffer[j] = buffer[j + 1];
        }
        count--;

        // 通知生产者
        pthread_cond_signal(&cond_producer);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, cons_thread;

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_producer, NULL);
    pthread_cond_init(&cond_consumer, NULL);

    // 创建生产者和消费者线程
    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&cons_thread, NULL, consumer, NULL);

    // 等待线程完成
    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_producer);
    pthread_cond_destroy(&cond_consumer);

    return 0;
}

运行结果(其中一个):

Produced: 0

Produced: 1

Produced: 2

Produced: 3

Produced: 4

Consumed: 0

Consumed: 1

Consumed: 2

Consumed: 3

Consumed: 4

Produced: 5

Produced: 6

Produced: 7

Produced: 8

Produced: 9

Consumed: 5

Consumed: 6

Consumed: 7

Consumed: 8

Consumed: 9

解释:

  • pthread_mutex_init(&mutex, NULL); 初始化互斥锁。
  • pthread_cond_init(&cond_producer, NULL);pthread_cond_init(&cond_consumer, NULL); 初始化生产者和消费者条件变量。

生产者线程操作

  • 生产者线程在向缓冲区添加数据之前,会先获取互斥锁 pthread_mutex_lock(&mutex);
  • 如果缓冲区已满,生产者线程会等待 pthread_cond_wait(&cond_producer, &mutex);,并释放互斥锁,直到消费者线程唤醒它。
  • 添加数据后,生产者线程会通知消费者线程 pthread_cond_signal(&cond_consumer);,并释放互斥锁 pthread_mutex_unlock(&mutex);

消费者线程操作

  • 消费者线程在消费数据之前,同样会获取互斥锁。
  • 如果缓冲区为空,消费者线程会等待 pthread_cond_wait(&cond_consumer, &mutex);,并释放互斥锁,直到生产者线程唤醒它。
  • 消费数据后,消费者线程会通知生产者线程 pthread_cond_signal(&cond_producer);,并释放互斥锁。

同步操作:

  • pthread_join(prod_thread, NULL);pthread_join(cons_thread, NULL); 用于等待两个线程完成执行。

  • 在程序结束时,销毁互斥锁和条件变量以释放资源。

8.6 屏障 (Barrier)

屏障 (Barrier) 是一种同步机制,它使得一组线程必须全部到达某个同步点后,才能继续执行。这通常用于并行计算中,当各个线程都完成了各自的一部分任务后,再继续执行下一步操作。

  • 等待 (Wait):线程在屏障处等待,直到所有其他线程都到达屏障。只有当所有线程都到达屏障时,它们才能继续执行。
c 复制代码
#include <stdio.h>
#include <pthread.h>

// 定义屏障
pthread_barrier_t barrier;

void *thread_function(void *arg) {
    int id = *(int *)arg;

    // 每个线程完成各自的计算任务
    printf("Thread %d is performing its task before the barrier\n", id);

    // 等待其他线程到达屏障
    pthread_barrier_wait(&barrier);

    // 所有线程都到达屏障后继续执行
    printf("Thread %d has crossed the barrier and is continuing\n", id);

    return NULL;
}

int main() {
    pthread_t threads[4];
    int ids[4] = {1, 2, 3, 4};

    // 初始化屏障,计数为4(表示4个线程需要同步)
    pthread_barrier_init(&barrier, NULL, 4);

    // 创建4个线程
    for (int i = 0; i < 4; i++) {
        pthread_create(&threads[i], NULL, thread_function, &ids[i]);
    }

    // 等待所有线程完成
    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁屏障
    pthread_barrier_destroy(&barrier);

    return 0;
}

运行结果(其中一个):

Thread 1 is performing its task before the barrier

Thread 3 is performing its task before the barrier

Thread 2 is performing its task before the barrier

Thread 4 is performing its task before the barrier

Thread 4 has crossed the barrier and is continuing

Thread 1 has crossed the barrier and is continuing

Thread 2 has crossed the barrier and is continuing

Thread 3 has crossed the barrier and is continuing

解释:

  • thread_barrier_init(&barrier, NULL, 4); 初始化屏障,并设置需要同步的线程数量为4。即所有4个线程都需要到达屏障后,才能继续执行。

  • 每个线程在执行完各自的任务后,调用 pthread_barrier_wait(&barrier); 进入屏障等待状态。

  • 当所有线程都到达屏障后,屏障会解除,所有线程将继续执行后续的任务。

  • pthread_join(threads[i], NULL); 用于等待所有线程完成执行,确保主线程在它们执行完毕后才继续。

  • pthread_barrier_destroy(&barrier); 在程序结束时销毁屏障,释放资源。

9. GDB进程调试

gdb 是 GNU Debugger,用于调试程序,包括设置断点、单步执行、查看变量、分析崩溃等。

sh 复制代码
# 编译程序时添加调试信息
gcc -g my_program.c -o my_program #记得加-g

# 启动 gdb 调试
gdb ./my_program

# 常用 gdb 命令
break main   # 设置断点在 main 函数
run          # 开始运行程序
next         # 单步执行
print var    # 打印变量值
bt           # 打印调用栈

简单案例(test.c):

c 复制代码
#include <stdio.h>

void fun() {
    int array[5];
    for (int i = 0; i <= 5; i++) {  // 错误:i 应该小于 5
        array[i] = i * 10;
        printf("array[%d] = %d\n", i, array[i]);
    }
}

int main() {
    fun();
    return 0;
}

运行结果:

liber@liber-VMware-Virtual-Platform:/home/c$ gcc -g test.c -o test

liber@liber-VMware-Virtual-Platform:/home/c$ gdb ./test

...

Type "apropos word" to search for commands related to "word"...

Reading symbols from ./test...

(gdb) break fun #设置断点

Breakpoint 1 at 0x1175: file test.c, line 3.

(gdb) run #开始运行程序

Starting program: /home/c/test

...

Breakpoint 1, fun () at test.c:3

3 void fun() {

(gdb) next

5 for (int i = 0; i <= 5; i++) { // 错误:i 应该小于 5

(gdb) next

6 array[i] = i * 10;

(gdb) print i #打印变量i

...

(gdb) next

7 printf("array[%d] = %d\n", i, array[i]);

(gdb) next

array[5] = 50 #数组越界

5 for (int i = 0; i <= 5; i++) { // 错误:i 应该小于 5

(gdb) next #下一步直接返回

9 }

(gdb) next

main () at test.c:13

13 return 0; #程序结束

(gdb)

说明:加粗字段是指令。

相关推荐
远游客07132 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<2 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟2 小时前
centos-stream9系统安装docker
linux·docker·centos
XH华2 小时前
初识C语言之二维数组(下)
c语言·算法
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
南宫生2 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_3 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
sanguine__3 小时前
Web APIs学习 (操作DOM BOM)
学习
落魄君子3 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘
冷眼看人间恩怨3 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget