手机自适应网站,深圳网站建设最专业,广州网络推广有限公司,嘉兴备案网站建设1. 进程的基础概念
1.1 进程是什么#xff1f;
定义#xff1a;
进程是操作系统管理的一个程序实例。它包含程序代码及其当前活动的状态。每个进程有自己的内存地址空间#xff0c;拥有独立的栈、堆、全局变量等。操作系统通过进程来分配资源#xff08;如 CPU 时间、内…1. 进程的基础概念
1.1 进程是什么
定义
进程是操作系统管理的一个程序实例。它包含程序代码及其当前活动的状态。每个进程有自己的内存地址空间拥有独立的栈、堆、全局变量等。操作系统通过进程来分配资源如 CPU 时间、内存等并管理任务的执行。
进程 vs 程序
程序静态的代码和数据存储在磁盘上。进程程序的一个运行实例包括程序的代码、活动状态、资源等。
1.2 进程的内存布局
一个典型进程的内存分布如下
内存段描述栈Stack存储函数调用时的局部变量、函数调用链等。栈从高地址向低地址增长。空间Gap堆和栈之间未分配的内存区域用于防止二者相互干扰。堆Heap用于动态内存分配如使用 malloc()堆从低地址向高地址增长。BSS段BSS Segment存储未初始化的全局变量和静态变量。数据段Data Segment存储已初始化的全局变量和静态变量。代码段Text Segment存储程序的代码即指令。
#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 中用于创建新进程的系统调用。它会复制当前进程创建一个新的子进程。子进程几乎完全与父进程相同但它们有独立的内存空间和资源。
#include stdio.h
#include unistd.hint 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 liberliber-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。
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/wait.hint main() {pid_t pid;// 使用 execl() 执行 /bin/lspid 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 -lperror(execl failed);exit(EXIT_FAILURE);} else if (pid 0) {// 父进程代码wait(NULL); // 等待子进程完成printf(execl() Child process completed.\n);}// 使用 execv() 执行 /bin/echopid 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() 执行 lspid fork();if (pid 0) {// 在子进程中执行printf(Child process (PID: %d) using execlp() to execute ls -a\n, getpid());execlp(ls, ls, -a, NULL); // 执行 ls -aperror(execlp failed);exit(EXIT_FAILURE);} else if (pid 0) {// 父进程代码wait(NULL); // 等待子进程完成printf(execlp() Child process completed.\n);}// 使用 execvp() 执行 echopid 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:
#include stdio.h
#include sys/wait.h
#include unistd.hint 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:
#include stdio.h
#include sys/wait.h
#include unistd.hint 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。
**匿名管道**适用于有亲缘关系的进程间通信比如父子进程。它的特点是临时性不会在文件系统中留下痕迹。
#include stdio.h
#include unistd.hint 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**是一种特殊的文件它允许无亲缘关系的进程通过管道文件进行通信。命名管道在文件系统中存在可以由任何进程打开和使用。
创建命名管道并写入数据
#include stdio.h
#include fcntl.h
#include unistd.h
#include string.h
#include sys/stat.hint 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); // 写入数据到FIFOclose(fd); // 关闭FIFOreturn 0;
}运行结果 光标一直闪烁等待被消费。 读取命名管道中的数据
#include stdio.h
#include fcntl.h
#include unistd.h
#include string.hint 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消息队列的键值唯一标识消息队列。创建或获取消息队列成功时返回消息队列标识符失败时返回 -1msgflg标志位用于设置权限和操作选项如 IPC_CREAT 表示创建消息队列。msgsnd()msqid消息队列标识符通过 msgget() 获取。向消息队列发送消息成功时返回 0失败时返回 -1msgp指向消息结构的指针包含消息类型和消息正文。msgsz消息正文的大小。msgflg控制操作的标志位如 IPC_NOWAIT 表示非阻塞发送。msgrcv()msqid消息队列标识符。从消息队列接收消息成功时返回接收到的消息大小失败时返回 -1msgp指向存储接收到消息的结构的指针。msgsz消息正文的大小。msgtyp指定要接收的消息类型0 表示接收队列中的第一个消息。msgflg控制操作的标志位如 IPC_NOWAIT 表示非阻塞接收。msgctl()msqid消息队列标识符。控制消息队列操作成功时返回 0失败时返回 -1cmd指定要执行的操作如 IPC_RMID 删除消息队列IPC_STAT 获取消息队列信息。buf可选参数用于存储或传递消息队列的状态信息。
详细操作说明 msgget(): 操作: 用于创建一个新的消息队列或获取一个现有的消息队列。 典型用法: 如果消息队列不存在且设置了 IPC_CREAT 标志则会创建一个新的消息队列。否则返回现有的消息队列标识符。 msgsnd(): 操作: 将消息发送到指定的消息队列中。 典型用法: 将消息结构 msgp 中的消息添加到 msqid 标识的队列中。如果设置了 IPC_NOWAIT则在队列已满时不会阻塞。 msgrcv(): 操作: 从指定的消息队列中接收消息。 典型用法: 从 msqid 标识的队列中接收消息并将其存储在 msgp 指向的结构中。如果 msgtyp 为 0接收队列中的第一个消息如果 msgtyp 为正数接收该类型的消息。 msgctl(): 操作: 执行消息队列的控制操作例如删除消息队列或查询队列的状态。 典型用法: 使用 IPC_RMID 标志删除消息队列或使用 IPC_STAT 获取队列的状态信息。
发送消息的程序sender.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);// 消息类型设为 1message.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;
}运行结果 liberliber-VMware-Virtual-Platform:/home/c$ gcc sender.c -o sender liberliber-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
#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;
}运行结果 liberliber-VMware-Virtual-Platform:/home/c$ gcc receiver.c -o receiver liberliber-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 类型的缓冲区用于存储或传递内存段的状态信息。
写入共享内存:
#include stdio.h
#include sys/ipc.h
#include sys/shm.h
#include string.hint 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()**将共享内存段附加到进程的地址空间使得进程可以访问该内存。返回一个指向该内存段的指针。
读取共享内存:
#include stdio.h
#include sys/ipc.h
#include sys/shm.hint 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指向信号量对象的指针用于销毁信号量。销毁信号量销毁信号量并释放与其相关的资源。仅适用于未用于进程间共享的信号量。
使用信号量同步两个线程:
#include stdio.h
#include pthread.h
#include semaphore.hsem_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;// 初始化信号量初始值为 1sem_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;
}liberliber-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. liberliber-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. liberliber-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_func1 和 thread_func2 中的代码块 printf(Inside critical section.\n); 信号量通过阻塞和唤醒机制协调多个线程的执行顺序。例如当 sem_wait(sem) 在一个线程中被调用时如果信号量的值为 0该线程将被阻塞直到另一个线程调用 sem_post(sem) 释放信号量。 当 thread_func1 或 thread_func2 开始执行时它们都会调用 sem_wait(sem)。此时信号量的初始值为 1表示资源即进入临界区的权限是可用的。 假设 thread_func1 先调用 sem_wait(sem)信号量值减为 0thread_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终端中断信号用户按 CtrlC 触发终止进程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终端停止信号用户按 CtrlZ 触发暂停进程停止进程SIGCONT继续执行已停止的进程继续执行如果之前停止则恢复执行SIGSTOP停止进程无法捕获或忽略停止进程SIGTTIN后台进程组试图从终端读取数据时引发的信号停止进程SIGTTOU后台进程组试图向终端写数据时引发的信号停止进程
5.2 信号处理函数
函数参数参数描述用途描述signal()signum要处理的信号编号如 SIGINT注册简单的信号处理程序用于指定一个信号处理函数当进程接收到特定信号时执行。handler信号处理函数的指针或特殊值 SIG_IGN忽略信号和 SIG_DFL默认处理sigaction()signum要处理的信号编号如 SIGINT注册信号处理程序替代 signalsigaction 是 signal 的增强版本允许更详细地控制信号处理行为。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()无无挂起进程执行直到接收到一个信号挂起进程的执行直到收到并处理一个信号。
简单案例
#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 (CtrlC), 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);// 创建一个子进程来演示 SIGCHLDif (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;
}如何运行和测试
按 CtrlC触发 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 (CtrlC), but not terminating the process. Caught SIGINT (CtrlC), 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. 第二个终端 liberliber-VMware-Virtual-Platform:~$ kill -SIGHUP 1933357 liberliber-VMware-Virtual-Platform:~$ kill -SIGTERM 1933357 6 常见的进程调度算法
6.1 先来先服务调度FCFS, First-Come, First-Served
先来先服务调度是一种最简单的调度算法。进程按照它们到达就绪队列的顺序依次运行先到达的进程先执行后到达的进程后执行。这个算法使用一个FIFOFirst In, First Out队列来管理进程顺序。
#include stdio.h// 定义进程结构体用来保存每个进程的相关信息
struct Process {int pid; // 进程IDint 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在实际中主要作为理论模型或通过预测实现。
#include stdio.h
#include limits.h // 用于获取整型的最大值INT_MAX// 定义进程结构体
struct Process {int pid; // 进程IDint 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; // 设置进程IDprocesses[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
最高优先级调度是一种调度策略进程根据其优先级进行调度优先级越高的进程越早被调度。该算法可以是抢占式的高优先级进程可以打断正在运行的低优先级进程或非抢占式的正在运行的进程不会被打断。
在非抢占式优先级调度中当前运行的进程会一直运行至完成而在抢占式调度中当一个高优先级的进程到达时它会立即中断当前正在运行的低优先级进程。
非抢占式优先级调度
#include stdio.h// 定义进程结构体
struct Process {int pid; // 进程IDint 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; // 设置进程IDprocesses[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 说明在非抢占式优先级调度中当一个进程开始执行后即使有更高本代码数字越小优先级越高优先级的进程到达也不会被打断直到该进程完成。
抢占式优先级调度
#include stdio.h
#include limits.h// 定义进程结构体
struct Process {int pid; // 进程IDint 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; // 设置进程IDprocesses[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立即抢占P1P1被挂起。P2开始执行。
时间点 4
P2完成执行它的执行时间是3个单位从时间点1到4。P1继续执行因为此时没有优先级更高的进程。
时间点 2
P3到达虽然它的优先级3低于P12但此时P1已经被P2抢占P3需要等待。P1继续执行。
时间点 8
P1完成执行P3开始执行。
时间点 12
P3完成执行。
6.4 轮转调度Round Robin, RR
**轮转调度Round Robin, RR**是一种时间片轮转调度算法。每个进程按照到达顺序进入队列并按照固定的时间片time quantum运行。当一个进程的时间片用完时它被放到队列的末尾等待下一轮的调度。如果在时间片内进程未完成执行它将在下一轮继续执行。轮转调度非常适合时间共享系统它在处理器上公平地分配CPU时间使得每个进程都有机会运行。
#include stdio.h// 定义进程结构体
struct Process {int pid; // 进程IDint 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; // 设置剩余执行时间为0processes[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; // 设置进程IDprocesses[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
命令示例
# 以更高优先级启动程序
nice -n -10 ./my_program# 调整正在运行的进程优先级
renice -n 5 -p 1234 # 将 PID 为 1234 的进程优先级降低# 查看
top运行结果
第一个终端 liberliber-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优先级和NInice值。
第三个终端 liberliber-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 FIFOFirst-In, First-Out页面置换算法
FIFO页面置换算法基于“先来先服务”的原则。它将最早进入内存的页面作为最先被置换的候选者不考虑页面的使用频率或最近使用时间。当需要置换页面时系统会选择最早进入内存的页面将其换出然后将新页面加载到该位置。使用队列Queue来跟踪页面的顺序。队列的头部是最早进入的页面尾部是最新进入的页面。当新页面进入时移除队列头部的页面将新页面添加到尾部。
简单案例
#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 LRULeast Recently Used页面置换算法
LRU页面置换算法基于“最近最少使用”原则选择最久未被访问的页面进行置换。系统需要记录每个页面的最后一次访问时间或访问顺序。当需要置换页面时选择记录中访问时间最久远的页面进行置换。
简单案例
#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 LFULeast Frequently Used页面置换算法
LFU页面置换算法基于“最少使用频率”原则选择在一段时间内使用次数最少的页面进行置换。为每个页面维护一个计数器每次访问该页面时计数器加1。当需要置换页面时选择计数器值最小的页面。
简单案例
#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() 是一种高效的内存管理技术可以将文件或设备映射到进程的地址空间允许进程通过内存直接访问文件内容。
简单案例
#include stdio.h
#include fcntl.h
#include sys/mman.h
#include sys/stat.h
#include unistd.hint 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线程释放互斥锁使得其他阻塞的线程可以继续执行。
#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, i1);}// 释放互斥锁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释放读锁或写锁允许其他等待的线程继续执行。
#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线程释放自旋锁允许其他等待的线程获取锁。
#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;
}运行结果 liberliber-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如果有等待的进程或线程则唤醒它们。
#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 通知所有等待线程。
#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)线程在屏障处等待直到所有其他线程都到达屏障。只有当所有线程都到达屏障时它们才能继续执行。
#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用于调试程序包括设置断点、单步执行、查看变量、分析崩溃等。
# 编译程序时添加调试信息
gcc -g my_program.c -o my_program #记得加-g# 启动 gdb 调试
gdb ./my_program# 常用 gdb 命令
break main # 设置断点在 main 函数
run # 开始运行程序
next # 单步执行
print var # 打印变量值
bt # 打印调用栈简单案例test.c
#include stdio.hvoid fun() {int array[5];for (int i 0; i 5; i) { // 错误i 应该小于 5array[i] i * 10;printf(array[%d] %d\n, i, array[i]);}
}int main() {fun();return 0;
}运行结果 liberliber-VMware-Virtual-Platform:/home/c$ gcc -g test.c -o test liberliber-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) 说明加粗字段是指令。