C语言进程(第二章,wait,sleep,waitpid,pthread_mutex_lock,pthread_mutex_unlock)

简介: C语言进程(第二章,wait,sleep,waitpid,pthread_mutex_lock,pthread_mutex_unlock)

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;
}

运行结果:

结果分析:

程序运行结果说明了以下几个重要的事实:

  1. 主程序中使用 fork() 函数创建了子进程,而在主进程和子进程的上下文中都打印了信息。由于进程调度顺序无法确定,因此这两个信息的顺序可能有时会颠倒。
  2. 子进程在输出 pid 后休眠了 1 秒钟,然后以退出码2终止。
  3. 父进程通过使用 wait() 等待子进程结束,并获取其退出状态码。这里,父进程首先阻塞自己,直到子进程终止。一旦该子进程终止,它的pid将作为 wait() 的返回值,则父进程回复执行状态并检索子进程所特定的退出状态,最后输出已终止的子进程pid 和其退出状态 (在本例中是2)。
  4. 程序正常结束,退出主函数并销毁剩余的内存空间。

总之,该程序演示了如何正确地使用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() 的功能如下:

  1. 如果该互斥量还没有被任何线程持有,它将分配给现在的线程,并向该线程返回 0。
  2. 如果该互斥量正在由另一个线程持有,则该线程将暂停执行,直到该互斥量变得可用为止。
  3. 如果试图递归获得相同的互斥量,则行为未定义(也就是说可能会导致死锁)。
  4. 如果在等待期间,线程接收到一个信号,则系统调用返回 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() 在控制台上打印出程序正常执行的消息,所以可以放心地在终端上观察程序的逐步运作及其结果。

相关文章
|
7月前
|
Linux
【Linux】—— 进程等待 wait&&waitpid
【Linux】—— 进程等待 wait&&waitpid
156 0
【Linux】—— 进程等待 wait&&waitpid
|
7月前
|
Linux
进程等待(wait和wait函数)【Linux】
进程等待(wait和wait函数)【Linux】
136 0
|
24天前
|
消息中间件 Unix Linux
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
49 6
|
3月前
|
存储 算法 Linux
C语言 多进程编程(一)进程创建
本文详细介绍了Linux系统中的进程管理。首先,文章解释了进程的概念及其特点,强调了进程作为操作系统中独立可调度实体的重要性。文章还深入讲解了Linux下的进程管理,包括如何获取进程ID、进程地址空间、虚拟地址与物理地址的区别,以及进程状态管理和优先级设置等内容。此外,还介绍了常用进程管理命令如`ps`、`top`、`pstree`和`kill`的使用方法。最后,文章讨论了进程的创建、退出和等待机制,并展示了如何通过`fork()`、`exec`家族函数以及`wait()`和`waitpid()`函数来管理和控制进程。此外,还介绍了守护进程的创建方法。
C语言 多进程编程(一)进程创建
|
3月前
|
网络协议 C语言
C语言 网络编程(十三)并发的TCP服务端-以进程完成功能
这段代码实现了一个基于TCP协议的多进程并发服务端和客户端程序。服务端通过创建子进程来处理多个客户端连接,解决了粘包问题,并支持不定长数据传输。客户端则循环发送数据并接收服务端回传的信息,同样处理了粘包问题。程序通过自定义的数据长度前缀确保了数据的完整性和准确性。
|
3月前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
3月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
3月前
|
C语言
C语言 网络编程(八)并发的UDP服务端 以进程完成功能
这段代码展示了如何使用多进程处理 UDP 客户端和服务端通信。客户端通过发送登录请求与服务端建立连接,并与服务端新建的子进程进行数据交换。服务端则负责接收请求,验证登录信息,并创建子进程处理客户端的具体请求。子进程会创建一个新的套接字与客户端通信,实现数据收发功能。此方案有效利用了多进程的优势,提高了系统的并发处理能力。
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
3月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。

相关实验场景

更多