C语言进程(第二章,wait,sleep,waitpid,pthread_mutex_lock,pthread_mutex_unlock,生产者消费者问题)
简介:本文讲解,C语言中的wait,sleep,waitpid,pthread_mutex_lock,pthread_mutex_unlock,函数在进程中的使用,还有经典的生产者消费者等问题的讲解。
相关在线编辑网站:https://www.ideone.com/whPQYr
wait
wait() 是一个 POSIX 标准库函数,用于在父进程中等待子进程的终止。它具有如下原型:
#include <sys/wait.h> pid_t wait(int *status);
- 参数 int *status 是一个指向 int 类型的指针,用来返回子进程的退出状态码
- 返回值是已终止子进程的 PID (如果有),或 -1(如果没有任何子进程)
父进程调用 wait() 函数会被阻塞,直到任一子进程结束运行。该子进程的资源将通过这个函数释放。一旦等待到子进程的终止,该进程就会返回退出状态码,并且从系统的进程表中删除已终止的子进程。
wait() 函数可以通过检查返回值是否为 -1 来确定子进程是否已经结束运行。 如果调用时没有未被收集回收的子进程并且也没有正在运行的子进程,则该函数会立即返回,并将错误代码 ECHILD 置于 errno。
以下是一个简单使用 wait() 函数的例子,其中子进程为1秒后输出PID并返回2的替代方案:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid; //定义一个进程id,用于保存 fork ()函数的返回值 int status; //定义一个整型变量,接收子进程退出状态 pid = fork(); //调用 fork 函数来创建子进程,并将返回值赋值给pid if (pid == -1) { //当pid=-1时,说明fork函数没有成功创建新进程,出现异常错误退出程序 printf("error: 创建进程失败 \n"); exit(1); } else if (pid == 0) { //当pid=0时,说明现在执行的是子进程。输出信息,等待1秒后终止。 printf("我是子进程,我的pid是 %d\n", getpid()); sleep(1); exit(2); } else { //当pid>0时,说明现在执行的是父进程。输出信息,并使用wait系统调用等待子进程结束,并获取子进程退出状态码。 printf("我是父进程,我的pid是 %d\n", getpid()); wait(&status); printf("已终止的子进程id是 %ld 返回状态:%d \n", (long) pid, WEXITSTATUS(status)); exit(0); //父进程结束 } return 0; }
运行结果:
结果分析:
程序运行结果说明了以下几个重要的事实:
- 主程序中使用 fork() 函数创建了子进程,而在主进程和子进程的上下文中都打印了信息。由于进程调度顺序无法确定,因此这两个信息的顺序可能有时会颠倒。
- 子进程在输出 pid 后休眠了 1 秒钟,然后以退出码2终止。
- 父进程通过使用 wait() 等待子进程结束,并获取其退出状态码。这里,父进程首先阻塞自己,直到子进程终止。一旦该子进程终止,它的pid将作为 wait() 的返回值,则父进程回复执行状态并检索子进程所特定的退出状态,最后输出已终止的子进程pid 和其退出状态 (在本例中是2)。
- 程序正常结束,退出主函数并销毁剩余的内存空间。
总之,该程序演示了如何正确地使用fork、wait系统调用来管理多个进程,从而实现了进程之间通信和协作的目标。同时,也启示了我们关于优化程序性能、提高系统可靠性的一些有效思路。
在这个例子中,父进程调用wait() 来等待被创建的子进程结束运行。当子进程完成时其返回值为2,并通过 WEXITSTATUS(status) 函数打印退出状态码。
很重要的一点:在使用wait函数等待子进程时,通常应该确保只有需要等待的子进程都结束运行后,再继续执行父进程的其他任务,否则会出现资源泄漏和错误码乱窜等情况。因此,在编写涉及到多个进程的程序时,请务必谨慎考虑并仔细设计系统架构。
在该程序中,首先调用 fork() 函数时,系统将创建一个新的子进程。由于是第一次执行PID为主进程(也称父进程)的ID(即 pid=15198),因此 PID 变量现在包含新的子进程 ID 值。
if (pid == -1) { printf("error: 创建进程失败 \n"); exit(1); } else if (pid == 0) { printf("我是子进程,我的PID是 %d\n", getpid()); sleep(1); exit(2); }
在子进程中, if 分支是由 pid == 0 的条件触发的。因此,在该块中定义并执行只针对子进程有效的操作(输出5号调试信息和等待1秒)。最后通过 exit() 来终止子进程。
else { printf("我是父进程,我的PID是 %d\n", getpid()); //4号调试信息 wait(&status); printf("已终止的子进程ID是 %ld 返回状态:%d \n", (long) pid, WEXITSTATUS(status)); //6号调试信息 exit(0); }
在 pid>0 时,说明现在执行的是父进程。 父进程打印了“我是父进程,我的PID是15198 ”这样的调试信息,并即刻调用 wait() 来等待子进程终止并获取其退出状态码。 然后,父进程再打印了一些输出来说明所等待的子进程已经终止。
sleep
sleep() 函数是C语言的一个标准库函数,用于使当前进程挂起一段固定的时间。函数原型如下:
#include <unistd.h> unsigned int sleep(unsigned int seconds);
其中,参数 seconds 表示希望休眠的秒数,返回值为 usleep() 中剩余时间的秒数。注意,如果 sleep () 返回0,则表示在指定的第一个时间段中途被唤醒。 如果超过了要求的秒数,将返回实际挂起时间的剩余部分。
当调用 sleep() 函数时,操作系统会阻止程序的继续执行并暂停程序的运行时间。 在等待所需时间后,函数返回以便程序可以恢复执行。 sleep() 可以用于延迟、定时等场景,也经常用于模拟需要长时间等待但不能(timeout)立即完成的程序处理。
以下是一个简单使用 sleep() 函数的例子:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("正在进行休眠...\n"); sleep(5); printf("休眠结束\n"); return 0; }
运行结果:
在这个例子中,程序在第二个printf()函数前休眠了5秒钟,然后输出“休眠结束”信息。
需要注意的是,在调用 sleep() 函数之前,应该保证其他的进行不会对代码执行造成影响。 否则,程序可能因等待时间过长而超时或得不到响应等意外情况。同时,进程的挂起会降低资源利用率,在开发实际需求中也需要谨慎使用_SLEEP()函数来保证系统性能和稳定性。
例题
例题一
编写一个程序,使用fork()创建两个子进程a和b,从父进程开始a、b执行顺序应为b后于a, 完成之后在屏幕上显示"b输出完毕"。
下面是一个使用 fork() 函数创建两个子进程 A 和 B 并让 B 后于 A 运行,并在结束之后输出 “b输出完毕” 的示例程序:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pidA, pidB; pidA = fork(); if (pidA == -1) { //当pidA=-1时 说明创建进程失败,输出错误信息。 printf("error: 创建进程A失败 \n"); return 1; } else if (pidA == 0) { //当pidA=0时,说明现在执行的是子进程 A。 printf("我是子进程A,我的PID是 %d.\n", getpid()); sleep(1); printf("子进程A退出.\n"); return 0; } pidB = fork(); if (pidB == -1) { //当pidB=-1时,说明创建进程B失败,输出错误信息并回收进程A的资源后退出 printf("error: 创建进程B失败 \n"); wait(&pidA); // 回收A的资源 return 1; } else if (pidB == 0) { //当pidB=0时,说明现在执行的是子进程B。 printf("我是子进程B,我的PID是 %d.\n", getpid()); sleep(2); printf("子进程B退出.\n"); return 0; } // 父进程等待两个子进程都结束 waitpid(pidA, NULL, 0); waitpid(pidB, NULL, 0); printf("b输出完毕\n"); return 0; }
运行结果:
该程序首先创建子进程A,然后在子进程A中按照顺序运行一些代码。接下来,它再次调用 fork() 函数创建子进程B来运行其他代码段。最后,父进程会等待两个子进程都结束,并打印出 “b输出完毕” 的信息。
需要注意的是,在此过程中,可能存在多进程竞争资源的问题。如果我们在访问共享内存、文件、网络等资源时对其进行加锁或使用其他同步机制就可以更好地解决这种问题。
例题二
编写一个程序,父进程创建5个子进程,并等待每个子进程完成后,计算并输出它们的运行时间。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/time.h> int main() { int i, status; // 创建5个子进程 for(i = 0; i < 5; i++) { pid_t pid = fork(); if(pid == 0) { // 子进程处理逻辑 struct timeval start, end; gettimeofday(&start, NULL); // 记录开始时间 sleep(i+1); // 模拟子进程执行耗时 gettimeofday(&end, NULL); // 记录结束时间 long runtime = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000; // 计算执行时间(毫秒) printf("Child %d finished in %ldms\n", i+1, runtime); exit(i+1); // 结束子进程并返回子进程编号 } } // 等待每个子进程执行并输出结果 for(i = 0; i < 5; i++) { pid_t pid = waitpid(-1, &status, 0); // 父进程暂停等待任意子进程完成 if(WIFEXITED(status)) { // 检查进程是否正常终止 int child_id = WEXITSTATUS(status); // 获取子进程的退出状态(也就是返回值,即子进程编号) printf("Parent: Child %d finished.\n", child_id); } else { // 检查进程是否异常终止 if (pid > 0) { perror("Child terminated with an error status"); } else if (pid == -1) { perror("Error while waiting for the child process to terminate"); } } } return 0; }
在上述代码中,WIFEXITED(status)函数用于检测waitpid()函数返回的子进程状态是否为正常退出,若是则调用 WEXITSTATUS(status)函数来获取子进程的退出状态(子进程编号),并打印相应信息。反之则利用perror()函数输出错误提示信息,说明子进程结束时发生了意外事件。通过这些更详细的调试信息,我们可以更好地处理和理解子进程的执行状况,在编写高效的多进程程序时非常有帮助。
运行结果:
当该代码运行时,父进程重复调用了五次waitpid()函数来等待每个子进程完成操作,并处理相应的返回状态。
当一个子进程执行完毕后,它退出并返回一个退出状态码给父进程。此时父进程与子进程分离,不再有联系。因为多个子进程的退出条目能够随机,因此使用waitpid() 函数是必要的,以确保子进程已经正常退出并且不会变成僵尸进程。
对于每个成功地结束的子进程,waitpid() 函数将返回一个合法的 pid 和相应的状态信息。这时就可以利用 wifexited() 和 wexitstatus() 宏来提取其终止状态并输出结果。如果子进程没有正常退出,则表明发生异常。使用 perror() 可以方便地生成错误提示并在程序中打印出来。
因此,通过正确的多进程编写和调试方式,此代码能够有效地创建、管理、控制和处理多个子进程的操作,正确打印并处理每个子进程的输出结果。
pthread_mutex_lock
pthread_mutex_lock() 函数是 posix 线程库中的一个同步函数,用于在代码块中获取对指定互斥量的独占访问权限。如果自上次保留后未解锁该互斥锁,则尝试获得锁将会失败并阻塞调用线程,直到该锁变为可用。
具体而言, pthread_mutex_lock() 的功能如下:
- 如果该互斥量还没有被任何线程持有,它将分配给现在的线程,并向该线程返回 0。
- 如果该互斥量正在由另一个线程持有,则该线程将暂停执行,直到该互斥量变得可用为止。
- 如果试图递归获得相同的互斥量,则行为未定义(也就是说可能会导致死锁)。
- 如果在等待期间,线程接收到一个信号,则系统调用返回 eintr 表示操作已被中断或取消。
好的,以下是一个简单的例子来说明 pthread_mutex_lock() 函数的用法。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define SIZE 10 int count = 0; int buffer[SIZE]; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* producer(void* arg) { int val = *(int*) arg; while (1) { pthread_mutex_lock(&mutex); // 获取互斥锁 if (count < SIZE) { buffer[count++] = val; // 向缓存中添加值 printf("Producer produces value: %d\n", val); } pthread_mutex_unlock(&mutex); // 释放互斥锁 } } void* consumer(void* arg) { while (1) { pthread_mutex_lock(&mutex); // 获取互斥锁 if (count > 0) { int val = buffer[--count]; printf("Consumer consumes value: %d\n", val); } pthread_mutex_unlock(&mutex); // 释放互斥锁 } } int main() { pthread_t producer_thread, consumer_thread; int prod_val = 5; pthread_create(&producer_thread, NULL, producer, &prod_val); pthread_create(&consumer_thread, NULL, consumer, NULL); pthread_join(producer_thread, NULL); pthread_join(consumer_thread, NULL); return 0; }
这个程序包含了一个生产者线程和一个消费者线程,它们在共享缓冲区中交换数据。为了避免访问资源时可能产生的冲突和竞争条件,使用了 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数来确保每个线程能够在操作共享资源时获得对互斥锁的独占访问。
- 生产者函数producer()循环执行以下动作:
- 如果缓存区 count 小于缓存区大小 SIZE,则向缓存中添加值,并将缓存计数器 count 加一。
- 打印出生产者向缓存区中写入的值,以便进行检查。
- 释放互斥锁
- 消费者函数consumer()循环执行以下动作:
- 如果缓存区 count 不为空(这里不是一个安全的判断方法,其仅适用于此简单示例),则从缓存中读取最近的数字,并将缓存计数器 count 减去 1。
- 打印消费者从缓存中读取的数字到控制台
- 释放互斥锁。
在主程序 main() 中,首先初始化互斥锁并启动了子线程以及传入给生产者函数的数据参数 (这里即整数类型的值5)。接着等待线程关闭后销毁生成的互斥锁和信号量。如果准确地跟踪所有线程将否如期按预期运行,将会发现缓存没有超出存储限制并且读取和写入的值是正确的,表明该程序实现了所需的线程同步机制。
在本示例中,在缓冲区的访问上使用互斥锁可以对竞态条件进行保护。调用 pthread_mutex_lock(&mutex) 时,如果锁当前未被任何线程占据,则获得互斥锁,并开始执行代码块中的语句。否则,该调用会阻塞,直到其他线程释放此锁为止。一旦缓存访问完成,则调用 pthread_mutex_unlock(&mutex) 即可将锁释放给其他线程使用。
运行结果:
由于这个程序是一个无限循环程序,所以在控制台上所输出的结果会不断地增加。在这里,我们只截取了一部分运行结果。
Producer produces value: 5 Consumer consumes value: 5 Producer produces value: 5 Consumer consumes value: 5 Producer produces value: 5 Consumer consumes value: 5 Producer produces value: 5 Consumer consumes value: 5 ...
以上就是本程序的部分运行结果。每次生产者创建一个新值并向缓存队列中添加该值后,消费者就从队列中删除这个值。随着时间的推移,这些操作会反复循环进行。由于程序包含使用互斥锁对共享资源进行写入和读取,并使用 printf() 在控制台上打印出程序正常执行的消息,所以可以放心地在终端上观察程序的逐步运作及其结果。