Linux系统编程之进程通信(四)

进程相关概念

程序和进程

程序是一组指令的集合,编写的代码以特定的编程语言形式存在。它是描述解决问题或执行特定任务的算法和逻辑的静态表示。程序通常以文本文件的形式存储在计算机上,并且需要被解释器或编译器转化为可执行的形式,以便计算机能够理解和执行其中的指令。

进程则是程序在计算机上的执行实例。它是计算机中正在运行的程序的动态执行过程。每当启动一个程序时,操作系统会为该程序创建一个独立的进程,为其分配资源(如内存和处理器时间),并执行程序的指令。一个计算机可以同时运行多个进程,每个进程都是相互独立的、拥有自己的内存空间和执行环境。每个进程都有一个唯一的标识符(进程ID),它可以用于操作系统进行进程管理、资源分配和通信。进程之间可以通过进程间通信(Inter-Process Communication,IPC)机制来进行数据交换和协作。

总结来说,程序是静态的指令集合,而进程是程序在计算机上的动态执行实例。进程是操作系统进行任务调度和资源管理的基本单位,它使计算机能够同时运行和管理多个程序。

并发

一个时间段中有多个进程都处于以启动运行到运行完毕之间的状态。但任一时刻只有一个进程在运行。宏观并行,微观串行

单道程序设计

在单道程序设计中,计算机系统一次只能执行一个程序,并按照程序的顺序逐步执行指令。程序被加载到计算机的内存中,并由中央处理器(CPU)按照顺序执行。执行过程中,程序从内存中读取指令和数据,经过处理后产生结果,并将结果写回到内存中。

多道程序设计

在多道程序设计中,计算机系统可以同时加载和执行多个程序。这些程序可以是独立的应用程序,每个程序都有自己的代码和数据。操作系统负责对这些程序进行任务调度,分配处理器时间和内存资源,以便它们可以并发地执行。

CPU和MMU

在这里插入图片描述
在这里插入图片描述

PCB进程控制块

进程控制块(Process Control Block,PCB)是操作系统中用于管理和维护进程信息的数据结构。每个正在执行的进程都有一个对应的 PCB,它包含了与该进程相关的各种属性和状态信息。
PCB 通常包含以下信息:

  1. 进程标识符(Process Identifier,PID):唯一标识该进程的数字或字符串。还包括用户ID和组ID。
  2. 文件描述符表。
  3. 进程状态(Process State):表示进程当前的状态,例如运行、就绪、阻塞等。
  4. 程序计数器(Program Counter,PC):指向当前正在执行的指令的地址。
  5. 寄存器集合(Register Set):包含进程的寄存器状态,包括通用寄存器、程序状态字等。
  6. 内存管理信息:包括进程的内存分配情况、内存限制和页面表等。
  7. 资源占用情况:记录进程所占用的资源,如打开的文件、网络连接等。
  8. 进程优先级(Process Priority):用于调度进程的优先级信息,决定了进程在竞争系统资源时的调度顺序。
  9. 进程等待队列指针(Process Wait Queue Pointers):指向等待某些事件的进程队列。

PCB 是操作系统管理进程的关键数据结构,它使操作系统能够跟踪和控制每个进程的执行。通过读取和更新 PCB 中的信息,操作系统可以进行进程的创建、调度、切换和终止等操作。PCB 的内容会根据进程的状态和执行情况而不断变化。

进程控制

fork函数

在操作系统中,fork() 是一个系统调用,用于创建一个新的进程。当执行 fork() 调用时,操作系统会创建当前进程的一个副本,包括进程的代码、数据和状态等信息。这个副本被称为子进程。

fork() 调用的返回值有以下特点:

  • 如果 fork() 调用成功,它将返回两次:一次在父进程中,一次在子进程中。
  • 在父进程中,fork() 返回子进程的进程 ID(PID)。这个值大于 0。
  • 在子进程中,fork() 返回 0,表示当前进程是子进程。
  • 如果 fork() 调用失败,它将只返回一次,返回一个负数值,表示出现了错误。

当 fork() 调用执行成功时,操作系统会创建一个新的进程,它是父进程的一个副本。父进程和子进程的执行是相互独立的,它们具有相同的代码和数据,但是有不同的进程 ID 和一些状态信息。

返回两次的原因是为了区分父进程和子进程,使它们可以根据返回值来执行不同的操作。父进程可以根据返回的子进程 ID 来跟踪和管理子进程,而子进程可以根据返回值是否为 0 来确定自己是子进程。

需要注意的是,父进程和子进程之间的执行顺序是不确定的,取决于操作系统的调度策略。它们可能以任意顺序执行,具体顺序由操作系统决定。

使用fork函数的一个示例,创建一个.c文件:

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

int main(int argc,char *arg[])
{
	printf("before fork---1\n");
	printf("before fork---2\n");
	printf("before fork---3\n");
	printf("before fork---4\n");
	printf("before fork---5\n");
	
	pid_t pid=fork();
	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid==0){
		printf("child is created,pid=%d,parent_pid=%d\n",getpid(),getppid());
	}else if(pid>0){
		printf("my child is %d,my pid=%d,my parent_pid=%d\n",pid,getpid(),getppid());
	}

	printf("=========end=========");
	return 0;
}

运行gcc fork.c -o fork然后终端输入./fork

before fork---1
before fork---2
before fork---3
before fork---4
before fork---5
this is a test:1
my child is 8790,my pid=8789,my parent_pid=7399
==========end===========
this is a test:1
child is created,pid=8790,parent_pid=8789
==========end===========

getpid()getppid()分别是获取当前进程pid和当前进程的父进程pid。

进程共享

在这里插入图片描述
父子进程都会复制一份,然后在自己的空间内修改自己的数据,如下所示,全局变量var=100,父进程修改为288后,子进程依然是100。

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

int var=100;

int main(int argc,char *arg[])
{

	pid_t pid=fork();
 
	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid>0){
    var=288;
    printf("parent var=%d\n",var);
		printf("my child is %d,my pid=%d,my parent_pid=%d\n",pid,getpid(),getppid());
	}else if(pid==0){
    printf("child var=%d\n",var);
		printf("child is created,pid=%d,parent_pid=%d\n",getpid(),getppid());
	}

	return 0;
}

输出:

parent var=288
my child is 9359,my pid=9358,my parent_pid=7399
child var=100
child is created,pid=9359,parent_pid=9358

父子进程共享文件描述符,mmap建立的映射区

exec函数族原理

使用exec后,子进程不在执行父进程后续代码,而是执行exec指定的程序。进程ID保持不变。
在这里插入图片描述

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


int main(int argc,char *arg[])
{

	pid_t pid=fork(); //创建子进程
 
	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid>0){  //父进程
    
    //execlp("ls","ls","-l","-h", NULL); //执行系统
    execl("./a.out","./a.out", NULL); //执行自己的
    perror("error\n");
		exit(1);
    
		//printf("my child is %d,my pid=%d,my parent_pid=%d\n",pid,getpid(),getppid());
	}else if(pid==0){  //子进程
 
 
		printf("child is created,pid=%d,parent_pid=%d\n",getpid(),getppid());
	}

	return 0;
}

孤儿进程和僵尸进程

孤儿进程(Orphan Process)和僵尸进程(Zombie Process)是在操作系统中常见的两种进程状态。

  1. 孤儿进程:
    当一个进程的父进程先于它自己退出或意外终止时,该进程成为孤儿进程。孤儿进程将由操作系统的 init 进程(通常是进程 ID 为 1 的进程)接管并成为其子进程。父死子活,孤儿进程不会成为系统的负担,因为它们将由 init 进程负责回收资源和终止。利用kill -9 pid杀死孤儿进程
    以下是一个孤儿进程的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include<sys/wait.h>

int main(int argc,char *arg[])
{
	pid_t pid=fork(); //创建子进程

	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid>0){  //父进程
    	printf("I am parent, my pid is %d\n",getpid());
    	sleep(5);
    	printf("====I am going to die====\n");
	}else if(pid==0){  //子进程
	   	 while(1){
      		printf("I am child, my parent id is %d\n",getppid());
      		sleep(1);
    	}
	}
	return 0;
}
  1. 僵尸进程:
    僵尸进程是指一个子进程在终止后,其父进程还没有调用 wait()waitpid() 等系统调用来获取子进程的终止状态信息。父活子死,在这种情况下,子进程的进程表条目仍然存在,但它不再执行任何代码,也不占用系统资源。僵尸进程只是等待父进程获取其终止状态(kill僵尸无效,但可以kill父进程使子僵尸进程被init回收)。

    僵尸进程通常是父进程没有适时处理子进程终止的结果。如果父进程不处理僵尸进程,那么系统中会出现大量的僵尸进程,这可能会浪费系统资源。

    要消除僵尸进程,父进程应该使用 wait()waitpid() 或类似的系统调用来获取子进程的终止状态,并将僵尸进程从进程表中删除。

下面是一个僵尸进程的例子:

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


int main(int argc,char *arg[])
{

	pid_t pid=fork(); //创建子进程
 
	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid>0){  //父进程
    while(1){
      printf("I am parent,pid=%d,myson=%d\n",getpid(),getppid());
      sleep(1);
    }
    
	}else if(pid==0){  //子进程
    printf("---I am child,my parent pid is %d, I am going to sleep 5s\n",getppid());
    sleep(5);
    printf("--child die--");
	}

	return 0;
}

需要注意的是,孤儿进程和僵尸进程是不同的概念。孤儿进程是指父进程提前终止而导致子进程成为孤儿,而僵尸进程是指子进程在终止后父进程没有及时处理导致的状态。在绝大多数情况下,孤儿进程会由 init 进程接管并处理,而僵尸进程需要父进程显式地处理。

  1. wait()回收子进程
    wait()函数是一个系统调用,用于父进程等待其子进程的终止,并获取子进程的退出状态。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

参数 status 是一个指向整型变量的指针,用于存储子进程的退出状态。如果不关心子进程的退出状态,可以将 status 设置为 NULL。

wait() 函数的作用是阻塞父进程,直到一个子进程终止。当子进程终止时,父进程会恢复执行,并从 wait() 函数返回。同时,父进程可以通过 status 参数获取子进程的退出状态。返回值是子进程的进程 ID(PID),如果出现错误,返回值为 -1。回收子进程资源。
示例:

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


int main(int argc,char *arg[])
{

	pid_t pid,wpid;
    int status;
  pid=fork(); //创建子进程
	if (pid==-1){
		perror("error\n");
		exit(1);
	}else if(pid>0){  //父进程
    wpid=wait(&status);
    if(wpid==-1){
      perror("wait error\n");
		  exit(1);
    }
    printf("parent wait finish\n");
    
	}else if(pid==0){  //子进程
    printf("---I am child,my parent pid is %d, I am going to sleep 5s\n",getppid());
    sleep(5);
    printf("--child die--\n");
	}
	return 0;
}

获取子进程退出值和异常终止信号:
<sys/wait.h> 头文件中,提供了一些宏(宏函数)用于处理子进程的退出状态。这些宏函数用于检查子进程的退出状态,并提供与进程终止相关的信息。

以下是常用的宏函数:

  1. WIFEXITED(status)
    检查子进程是否正常终止(通过调用 exit() 或返回 main())。如果子进程正常终止,则返回一个非零值。

  2. WEXITSTATUS(status)
    获取子进程的退出状态码。只有在 WIFEXITED(status) 为真时才可用,用于提取子进程的退出状态码。

  3. WIFSIGNALED(status)
    检查子进程是否由于信号而终止。如果子进程是因为未捕获的信号终止的,则返回一个非零值。

  4. WTERMSIG(status)
    获取导致子进程终止的信号编号。只有在 WIFSIGNALED(status) 为真时才可用,用于获取终止子进程的信号编号。

  5. WCOREDUMP(status)
    检查子进程是否生成了核心转储文件。如果子进程生成了核心转储文件,则返回一个非零值。

这些宏函数用于在处理子进程的退出状态时提供了一些便利。您可以使用这些宏函数来检查子进程是如何终止的、获取退出状态码和相关的信号信息。

以下是一个示例,演示了如何使用这些宏函数处理子进程的退出状态:

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
        // 子进程执行的代码
        printf("Child process\n");
        sleep(3);
        exit(123);  // 子进程退出并返回状态码 123
    } else {
        // 父进程执行的代码
        printf("Parent process\n");
        int status;
        //pid_t child_pid=wait(NULL); //不关心子进程结束的原因,只能回收一个进程
        pid_t child_pid = wait(&status);  // 父进程等待子进程终止并获取退出状态
        if (child_pid == -1) {
            perror("wait error");
            exit(1);
        }
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        }
        if (WIFSIGNALED(status)) {
            printf("Child process terminated by signal: %d\n", WTERMSIG(status));
        }
        if (WCOREDUMP(status)) {
            printf("Child process dumped core\n");
        }
    }

    return 0;
}

在上述示例中,通过使用 WIFEXITEDWEXITSTATUSWIFSIGNALEDWTERMSIGWCOREDUMP 宏函数,父进程根据子进程的退出状态提供了相应的输出。

  1. waitpid()回收子进程
    waitpid() 是一个用于等待指定子进程终止并回收其资源的系统调用函数。它提供了比 wait() 更灵活的方式来处理子进程。

函数原型如下:

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数说明:

  • pid:指定要等待的子进程的进程 ID。具体取值有以下几种情况:
    • < -1:等待进程组 ID 为 pid 的任一子进程。
    • -1:等待任一子进程,类似于 wait()
    • 0:等待与调用进程在同一个进程组的任一子进程。
    • > 0:等待进程 ID 为 pid 的子进程。
  • status:与 wait() 函数相同,是一个指向整型变量的指针,用于存储子进程的退出状态。
  • options:指定额外的选项来控制 waitpid() 的行为。常用的选项包括:
    • WCONTINUED:等待一个被暂停后已经继续执行的子进程。
    • WNOHANG:如果没有子进程终止,立即返回而不阻塞。
    • WUNTRACED:等待一个被暂停的子进程。

waitpid() 函数在等待指定子进程终止时会阻塞父进程。当子进程终止后,父进程会从 waitpid() 函数返回,并且可以通过 status 参数获取子进程的退出状态。同时,waitpid() 函数会回收子进程的资源,防止其成为僵尸进程。

以下是一个示例代码,演示了父进程使用 waitpid() 函数等待指定子进程终止,并获取子进程的退出状态:

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
        // 子进程执行的代码
        printf("Child process\n");
        sleep(3);
        exit(123);  // 子进程退出并返回状态码 123
    } else {
        // 父进程执行的代码
        printf("Parent process\n");
        int status;
        pid_t child_pid = waitpid(pid, &status, 0);  // 父进程等待指定子进程终止并获取退出状态
        if (child_pid == -1) {
            perror("waitpid error");
            exit(1);
        }
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

在上述示例中,父进程通过调用 waitpid() 函数等待指定的子进程终止,并使用 status 参数获取子进程的退出状态。waitpid(pid, &status, 0) 指定等待进程 ID 为 pid 的子进程,并使用阻塞模式等待子进程终止。

需要注意的是,与 wait() 函数类似,waitpid() 函数也会阻塞父进程,直到子进程终止。如果不希望父进程被阻塞,可以使用 WNOHANG 选项,以非阻塞方式调用 waitpid() 函数。

进程间通信

通信常见方式

进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和信息共享的机制。在操作系统中,进程间通信是实现进程间相互协作和数据交换的重要手段。

以下是几种常见的进程间通信方式:

  1. 管道(Pipe):
    管道是一种半双工的通信机制,用于具有亲缘关系的父子进程或者兄弟进程之间的通信。它可以在进程之间传递数据,其中一个进程作为写入端,另一个进程作为读取端。

  2. 命名管道(Named Pipe):
    命名管道也是一种进程间通信的机制,但不限于具有亲缘关系的进程。命名管道是一种特殊类型的文件,多个进程可以通过打开该文件来进行通信。

  3. 信号(Signal):
    信号是一种异步的通信机制,用于在进程之间传递简短的消息。通过向目标进程发送信号,可以触发目标进程执行相应的信号处理程序。

  4. 共享内存(Shared Memory):
    共享内存是一种高效的进程间通信方式,它允许多个进程共享同一块物理内存区域。进程可以直接读取和写入共享内存,而无需通过复制数据来进行通信。

  5. 消息队列(Message Queue):
    消息队列是一种在进程间传递数据的机制,其中数据被组织为消息,并存储在内核中的队列中。多个进程可以通过读取和写入消息队列来进行通信。

  6. 信号量(Semaphore):
    信号量是一种用于控制对共享资源的访问的机制。它可以用来实现进程之间的同步和互斥,并确保共享资源的正确访问。

  7. 套接字(Socket):
    套接字是一种通用的进程间通信机制,通常用于不同主机或网络中的进程之间的通信。它可以在网络上进行数据交换,并支持不同的通信协议。

  8. mmap映射:非血缘关系进程间通信。

管道

管道可以实现进程间的数据传输和共享,一个进程将数据写入管道,而另一个进程从管道中读取数据。这种通信方式可以用于实现诸如进程间协作、数据传递、父子进程通信等应用场景。内核借助环形队列机制,使用内核缓冲区实现。特质:伪文件,管道中的数据只能一次读取。数据在管道中,只能单向流动。局限性:自己写不能自己读;数据不可以反复读取;半双工通信;血缘关系进程间可用。

pipe函数

pipe()函数是一个系统调用,用于在操作系统中创建一个管道。它的原型定义在 <unistd.h> 头文件中。

#include <unistd.h>

int pipe(int pipefd[2]);

pipe()函数接受一个整型数组 pipefd[2] 作为参数,该数组用于存储管道的两个文件描述符(文件描述符是操作系统为每个打开的文件或管道分配的唯一标识符)。

pipefd[0] 是管道的读取端(读取数据时使用),pipefd[1] 是管道的写入端(写入数据时使用)。

调用 pipe() 函数成功后,会创建一个匿名管道,并将管道的两个文件描述符分别存储在 pipefd 数组中。这样,通过 pipefd[0]pipefd[1],进程可以进行管道的读写操作。

以下是一个简单的示例,演示了如何使用 pipe() 函数创建管道并进行进程间通信:

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

int main() {
    int pipefd[2];
    char buffer[256];
    pid_t pid;

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        close(pipefd[1]); // 关闭写入端
        read(pipefd[0], buffer, sizeof(buffer));
        printf("子进程读取到的数据:%s\n", buffer);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        close(pipefd[0]); // 关闭读取端
        write(pipefd[1], "Hello, child process!", 22);
        close(pipefd[1]);
        wait(NULL); // 等待子进程结束
        exit(EXIT_SUCCESS);
    }

    return 0;
}

在上面的示例中,首先调用 pipe(pipefd) 创建一个管道,并将读取端的文件描述符存储在 pipefd[0] 中,写入端的文件描述符存储在 pipefd[1] 中。

然后,通过 fork() 创建一个子进程。在子进程中,关闭写入端的文件描述符 pipefd[1],并使用 read() 从管道的读取端 pipefd[0] 读取数据到 buffer 数组中。

在父进程中,关闭读取端的文件描述符 pipefd[0],然后使用 write() 将数据写入管道的写入端 pipefd[1]

最后,父进程通过 wait(NULL) 等待子进程结束,确保子进程读取完数据后再退出。

管道的读写行为是基于先进先出(FIFO)原则的,遵循以下规则:

  1. 写入数据:当一个进程向管道写入数据时,数据被添加到管道的写入端,并且存储在管道的缓冲区中。写入操作是阻塞的,当管道缓冲区已满时,写入操作将被阻塞,直到有足够的空间来写入数据。如果所有指向管道写入端的文件描述符都被关闭,进程向管道写入数据将引发信号(SIGPIPE)或导致写入操作失败。

  2. 读取数据:当一个进程从管道读取数据时,它从管道的读取端读取先前写入的数据。读取操作也是阻塞的,当管道缓冲区为空时,读取操作将被阻塞,直到有数据可供读取。如果所有指向管道读取端的文件描述符都被关闭,进程从管道读取数据将返回0,表示到达了管道的末尾(End-of-File)。
    示例,父进程没有往管道里写数据,所以子进程一直在等待,直到管道里面有数据:

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int ret;
    int fd[2];
    pid_t pid;
    char buf[1024];
    
    char *str="hello pipe\n";
    
    ret=pipe(fd);  //创建一个管道
    if (ret==-1)
        sys_err("pipe error");
        
    pid=fork(); 
    
    if (pid>0){
        close(fd[0]);  //关闭读端
        sleep(3);
        write(fd[1],str,strlen(str));
        close(fd[1]);
    }else if(pid==0){
        close(fd[1]);
        ret=read(fd[0],buf,sizeof(buf));
        write(STDOUT_FILENO,buf,ret);
        close(fd[0]);
    }
  
	  return 0;
}
  1. 关闭文件描述符:当所有指向管道写入端和读取端的文件描述符都被关闭时,管道被认为是关闭的。对于写入端而言,关闭意味着不再有数据写入管道。对于读取端而言,关闭意味着不再有数据可读取。一旦管道被关闭,任何进一步的写入操作都将失败,而读取操作将返回0,表示到达了管道的末尾。

需要注意的是,管道是有限容量的,当写入数据的速度超过读取数据的速度时,管道的缓冲区可能会溢出。此时,写入操作可能会被阻塞或丢失数据。因此,在使用管道进行进程间通信时,需要确保适当地控制数据的读取和写入速度,以避免数据丢失或阻塞的情况发生。
在这里插入图片描述

练习

使用管道实现父子进程间通信,完成: ls | wc -l。假定父进程实现 ls,子进程实现 wc。这个命令 ls | wc -l 是一个常见的 Shell 命令组合,用于统计当前目录下的文件数量(不包括子目录)。

  • ls 命令用于列出当前目录下的文件和文件夹。默认情况下,ls 命令会将文件和文件夹的名称输出到标准输出(终端)。
  • | 是管道符号,它将 ls 命令的输出连接到下一个命令的输入。
  • wc 命令用于统计文本数据的行数、字数和字符数。在这个命令中,使用 -l 选项来指定只统计行数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int ret;
    int fd[2];
    pid_t pid;
    char buf[1024];
    
    char *str="hello pipe\n";
    
    ret=pipe(fd);  //创建一个管道
    if (ret==-1)
        sys_err("pipe error");
        
    pid=fork(); 
    
    if (pid>0){  //父进程
        close(fd[0]);  //关闭读端
        dup2(fd[1],STDOUT_FILENO);
        execlp("ls","ls",NULL);
        sys_err("parent execlp error");

    }else if(pid==0){  //子进程
        close(fd[1]);  //关闭写端
        dup2(fd[0],STDIN_FILENO);
        execlp("wc","wc","-l",NULL);
        sys_err("child execlp error");

    }
  
	  return 0;
}

使用管道实现兄弟进程间通信,父进程回收子进程。完成: ls | wc -l。兄:ls,弟:wc -l。要求:循环创建多个子进程。

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int ret;
    int fd[2];
    pid_t pid;
    int i;
    
    ret=pipe(fd);  //创建一个管道
    if (ret==-1)
        sys_err("pipe error");
        
    for(i=0;i<2;i++) //循环创建两个子进程
    {
        pid=fork();
        if(pid==-1){
            sys_err("fork error");
        }
        if(pid==0){ //子进程出口
            break;
        }
    }
        
    if(i==2){ //父进程 wait等待子进程 因为循环体执行完i=1后,i会自增所以i=2
        
        close(fd[0]);
        close(fd[1]);
        wait(NULL);
        wait(NULL);
        
    }else if(i==0){ //兄进程
    
        close(fd[0]);  //关闭读端
        dup2(fd[1],STDOUT_FILENO);
        execlp("ls","ls",NULL);
        sys_err("bro execlp error");
    
    }else if(i==1){ //弟进程
    
        close(fd[1]);  //关闭写端
        dup2(fd[0],STDIN_FILENO);
        execlp("wc","wc","-l",NULL);
        sys_err("bro execlp error");
    }
 
	  return 0;
}

命名管道

命名管道(Named Pipe)和管道(Pipe)是两种不同的进程间通信(IPC)机制,它们具有以下区别:

  1. 创建方式:

    • 匿名管道:在操作系统中使用 pipe() 系统调用来创建,它是一种临时的、匿名的通信管道,只能在具有共同祖先的进程之间使用。
    • 命名管道:使用 mkfifo 命令或 mkfifo() 系统调用在文件系统中创建,它具有关联的路径名,可以由不相关的进程之间使用。
  2. 持久性:

    • 匿名管道:存在于进程的内存空间中,通常在进程结束时自动被销毁。
    • 命名管道:存在于文件系统中,即使进程结束或系统重启,命名管道仍然存在,可以持续使用。
  3. 进程间关系:

    • 匿名管道:只能用于具有共同祖先的进程之间的通信,通常用于父进程和子进程之间的通信。
    • 命名管道:可以用于不相关的进程之间的通信,只要它们知道并具有对应的路径名。
  4. 数据传输:

    • 匿名管道和命名管道都遵循先进先出(FIFO)原则,数据以字节流的形式传输。
    • 匿名管道和命名管道都可以进行读取和写入操作,通过文件描述符或句柄进行数据的传输。
  5. 通信范围:

    • 匿名管道:仅适用于单个系统中的进程间通信。
    • 命名管道:可适用于同一系统中的进程间通信,也可用于网络上的不同系统之间的进程通信。

总的来说,匿名管道适用于具有共同祖先的进程之间的临时通信,而命名管道适用于不相关的进程之间的持久化通信,并且可以通过文件系统路径名进行访问。选择使用哪种管道取决于具体的通信需求和进程间关系。以下代码会创建一个管道,创建出来的fifo文件是伪文件,不占内存。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int ret=mkfifo("mytestfifo",0664);
    if(ret==-1){
        sys_err("fifo error!\n");
    }
  
	  return 0;
}

输出:

ll
prw-rw-r–. 1 kewang kewang 0 7月 10 16:54 mytestfifo

一个常见的命名管道的例子是使用命名管道来实现进程间的聊天程序。以下是一个简化的示例:

假设有两个进程,一个是发送消息的进程(Sender),另一个是接收消息的进程(Receiver)。

Sender 进程:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    // 创建命名管道
    mkfifo("myfifo", 0666);

    // 打开管道的写入端
    int fd = open("myfifo", O_WRONLY);

    // 发送消息
    char message[] = "Hello, Receiver!";
    write(fd, message, sizeof(message));
    
    // 关闭管道
    close(fd);

    return 0;
}

Receiver 进程:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    // 打开管道的读取端
    int fd = open("myfifo", O_RDONLY);

    // 接收消息
    char message[100];
    read(fd, message, sizeof(message));
    printf("Received message: %s\n", message);
    
    // 关闭管道
    close(fd);

    // 删除命名管道
    unlink("myfifo");

    return 0;
}

在这个例子中,Sender 进程创建了一个命名管道(“myfifo”),并打开了管道的写入端。然后,它向管道写入一条消息。

Receiver 进程打开了同样的命名管道(“myfifo”),并打开了管道的读取端。然后,它从管道中读取接收到的消息,并将其打印出来。

注意,Sender 和 Receiver 进程在操作管道之前,需要先创建相同的命名管道,并使用相同的路径名。在结束通信后,可以通过调用 unlink() 函数来删除命名管道。

这个例子展示了如何使用命名管道实现简单的进程间通信,通过命名管道,Sender 进程可以向 Receiver 进程发送消息,Receiver 进程可以接收并处理这些消息。

共享存储映射

共享存储映射(Shared Memory Mapping)是一种进程间通信的机制,它允许多个进程共享同一块内存区域,从而实现数据的快速共享和交换。

在共享存储映射中,操作系统将一块内存区域映射到多个进程的虚拟地址空间中,使得这些进程可以直接访问同一块物理内存,而无需进行复制或通过其他形式的数据传输。

共享存储映射的主要步骤如下:

  1. 创建共享内存:使用系统调用(例如shmget)或库函数(例如shm_open)创建一个共享内存对象,并指定它的大小。

  2. 将共享内存映射到进程地址空间:使用系统调用(例如mmap)将共享内存映射到进程的虚拟地址空间中。这样,多个进程就可以通过访问相同的虚拟地址来访问共享内存。

  3. 进程间访问共享内存:每个进程都可以通过读写映射到它的地址空间的共享内存来进行数据的读取和写入。对共享内存的修改将立即反映在其他所有进程的访问中。

  4. 解除映射和删除共享内存:当不再需要共享内存时,可以使用系统调用(例如munmap)将共享内存从进程的地址空间中解除映射。最后,可以使用系统调用(例如shmctl)或库函数(例如shm_unlink)来删除共享内存对象。

共享存储映射在并发编程、多进程协作、共享数据等场景中具有高效和灵活的特性,但同时也需要注意进程间同步和互斥的问题,以避免数据竞争和一致性问题。

文件用于进程间的读写

以下是一个利用文件进行父子进程通信的例子,子写入文件,父读出文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int fd1,fd2;
    
    pid_t pid;
    
    char buf[1024];
    
    char *str="========test for shared fd in parent child process========\n";
    
    pid=fork();
    
    if (pid<0){
        sys_err("fork error");
        exit(1);
    }else if (pid==0){  //子进程
    
        fd1=open("test.txt",O_RDWR);
        if (fd1<0){
            sys_err("fd1 open error");
            exit(1);
        }
        write(fd1,str,strlen(str));
        printf("child wrote over---\n");

    }else if(pid>0){  //父进程
        fd2=open("test.txt",O_RDWR);
        if (fd2<0){
            sys_err("fd2 open error");
            exit(1);
        }
        sleep(1);
        int len=read(fd2,buf,sizeof(buf));
        write(STDOUT_FILENO,buf,len);
        wait(NULL);

    }
  
	  return 0;
}

存储映射I/O

存储映射I/O(Memory-mapped I/O)是一种通过将设备的寄存器或缓冲区映射到进程的地址空间来进行输入和输出操作的技术。

在存储映射I/O中,设备的寄存器或缓冲区被映射到进程的虚拟地址空间的一部分。进程可以像访问内存一样直接读取和写入这些地址,而无需通过显式的I/O指令来与设备进行通信。

使用存储映射I/O的主要步骤如下:

  1. 打开设备文件:通过打开设备文件,将设备文件映射到进程的地址空间。可以使用标准的文件I/O函数(例如open())来打开设备文件。

  2. 映射设备文件:使用系统调用(例如mmap())将设备文件映射到进程的地址空间。在映射过程中,指定映射的起始地址、长度和访问权限等参数。

  3. 进行I/O操作:通过直接读取和写入映射的地址,进行设备的输入和输出操作。进程可以使用指针和偏移量来访问设备的寄存器或缓冲区。

  4. 解除映射和关闭设备文件:在不再需要进行I/O操作时,使用系统调用(例如munmap())解除设备文件的映射。然后,使用标准的文件I/O函数(例如close())关闭设备文件。

存储映射I/O可以提高I/O操作的效率,因为它允许进程直接访问设备的寄存器或缓冲区,而无需进行系统调用和数据拷贝。它常用于需要高性能和实时性的应用程序,如嵌入式系统、驱动程序开发等场景。但同时也需要谨慎处理并发访问和同步的问题,以避免数据的竞争和一致性问题。

在这里插入图片描述

mmap函数

mmap() 函数是一个系统调用,用于将文件或其他对象映射到进程的地址空间中。

函数原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

  • addr:映射的首选地址。通常设置为 NULL,表示由系统选择映射的地址(自动分配)。
  • length:映射区域的长度,以字节为单位(映射大小)。
  • prot:映射区域的保护方式,即对映射的内存区域设置的访问权限,可以使用以下常量进行位或运算:
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
  • flags:映射区域的标志位,控制映射方式和特性,可以使用以下常量进行位或运算:
    • MAP_SHARED:与其他映射该文件的进程共享更新
    • MAP_PRIVATE:创建一个私有的写时复制副本
    • MAP_FIXED:尝试在给定地址处创建映射,若不可用则失败
  • fd:要映射的文件描述符,或者使用 -1 表示不映射文件。
  • offset:文件映射的偏移量,表示从文件中的哪个位置开始映射,通常设置为 0表示映射文件全部。注意需要是4k的整数倍。

返回值:

  • 成功时,返回映射区域的起始地址
  • 失败时,返回 MAP_FAILED 宏,表示映射失败。

mmap() 函数可以用于将文件映射到进程的地址空间,也可以用于创建匿名内存映射,用于共享内存或进行进程间通信。

在使用 mmap() 函数时,需要注意对映射区域的访问权限和保护方式进行正确设置,同时需要注意进行错误处理和适当的同步机制,以确保多个进程之间的数据一致性。

munmap() 函数用于解除通过 mmap() 函数创建的内存映射关系。它接受两个参数:映射区域的起始地址和映射区域的长度。

函数原型如下:

int munmap(void *addr, size_t length);
  • addr:映射区域的起始地址,通常是 mmap() 函数的返回值。
  • length:映射区域的长度,即 mmap() 函数调用时指定的长度。

munmap() 函数的作用是将之前通过 mmap() 函数映射到内存中的区域解除映射,使该内存区域不再与文件或其他对象关联。解除映射后,对该内存区域的访问将会导致未定义的行为。

在调用 munmap() 函数后,操作系统会释放映射区域所占用的内存资源,并清除与该映射相关的任何内核数据结构。

通常,在使用完映射内存后,应该调用 munmap() 函数来显式解除映射,以便正确释放资源并确保内存一致性。

mmap建立映射区

下面这段代码实现了将文件映射到内存中,并对映射的内存进行读写操作的功能。

首先,头文件部分包含了需要使用的标准库和系统库的头文件。

接下来,定义了一个辅助函数 sys_err,用于打印错误信息并退出程序。

main 函数中:

  1. 声明了一个指针变量 p,用于指向映射的内存区域。
  2. 打开文件 “testmap”,如果打开失败则调用 sys_err 函数报错。
  3. 使用 open 函数的返回值 fd 创建或截断文件,并设置访问权限为读写模式。
  4. 使用 ftruncate 函数将文件大小设置为 20 字节。
  5. 使用 lseek 函数获取文件的大小,并将结果保存在 len 变量中。
  6. 使用 mmap 函数将文件映射到内存中。传入的参数包括:
    • NULL:映射区域的首选地址,让系统自动选择合适的地址。
    • len:映射区域的长度,即文件的大小。
    • PROT_READ | PROT_WRITE:映射区域的保护方式,可读可写。
    • MAP_SHARED:与其他映射该文件的进程共享更新。
    • fd:要映射的文件描述符。
    • 0:文件映射的偏移量,从文件的起始位置开始映射。
      如果映射失败,调用 sys_err 函数报错。
  7. 通过指针 p 对映射内存进行读写操作,将字符串 “hello map\n” 复制到映射区域中。
  8. 使用 munmap 函数解除映射,并检查是否成功。如果解除映射失败,调用 sys_err 函数报错。
  9. 返回 0 表示程序执行成功。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    char *p=NULL;
    int fd;
    fd=open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
    if(fd==-1){
        sys_err("file error");
    }
    
    //lseek(fd,10,SEEK_END);
    //write(fd,"\0",1);
    
    ftruncate(fd,20);
    int len=lseek(fd,0,SEEK_END);
    p=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p==MAP_FAILED){
        sys_err("MAP_FAILED error");
    }
    
    //使用p对文件进行读写操作
    strcpy(p,"hello map\n");
    printf("-----%s\n",p);
    
    int ret=munmap(p,len);
    if(ret==-1){
        sys_err("munmap error");
    }
  
	  return 0;
}

mmap注意事项:
在这里插入图片描述

  1. 可以在打开文件时使用 O_CREAT 标志来创建新文件,并将其映射到内存中。当使用 open() 函数打开一个文件时,如果文件不存在,则根据给定的权限和标志创建新文件。然后可以使用 mmap() 函数将该文件映射到内存中。
    在这里插入图片描述

  2. 如果在打开文件时使用 O_RDONLY 标志,并且在 mmap() 调用中指定了 PROT_READ | PROT_WRITE 访问权限,会发生以下情况:

    • 如果文件以只读方式打开,PROT_WRITE 权限将被忽略,即使在 mmap() 中指定了写权限,也无法对映射区域进行写入操作。
    • 如果文件以读写方式打开,PROT_READPROT_WRITE 权限将同时有效,即可对映射区域进行读取和写入操作。
      在这里插入图片描述
      在这里插入图片描述
  3. 如果在调用 mmap() 函数后关闭文件描述符,对映射本身没有影响。关闭文件描述符不会影响已经建立的映射关系,因为映射是建立在文件的文件描述符上的(地址),而不是在文件描述符上。

  4. 文件偏移量在调用 mmap() 函数时通常应该是页大小(通常是 4096 字节)的整数倍。当等于1000时会报错,参数无效。

  5. 对于映射内存的越界操作是不安全的,可能会导致未定义的行为。当对映射内存进行越界访问时,可能会覆盖或修改其他数据,导致数据损坏或程序崩溃。因此,应该始终确保在映射内存范围内进行有效和合法的访问。

  6. munmap用于释放的地址必须是mmap申请返回的地址。

  7. mmap() 调用可能会失败的情况包括:

    • 文件打开失败:如果在调用 open() 函数时指定的文件不存在或权限不足,导致无法打开文件,则 mmap() 调用可能会失败。
    • 内存不足:如果系统内存不足以满足映射区域的大小需求,则 mmap() 调用可能会失败。
    • 参数错误:如果传递给 mmap() 函数的参数无效或不合法,例如非法的地址或长度,也可能导致 mmap() 调用失败。
  8. 如果不检测 mmap() 的返回值,可能会导致潜在的问题和错误。如果 mmap() 调用失败,返回值将是 MAP_FAILED,即一个特定的宏定义。如果不对返回值进行检查,可能会导致后续对映射内存的访问出现错误或未定义的行为。因此,始终建议检查 mmap() 的返回值,并根据返回值来处理错误情况。

mmap父子进程间的通信

mmap() 函数可以用于实现父子进程之间的共享内存通信。通过将相同的文件映射到父子进程的地址空间中,它们可以使用映射区域进行通信和共享数据。

以下是一个简单的示例,演示了如何在父子进程间使用 mmap() 进行通信:

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>

#define SHARED_MEM_SIZE 4096

int main() {
    int fd;
    pid_t pid;
    void *shared_mem;
    const char *message = "Hello from parent process";

    // 创建共享内存文件
    fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        return 1;
    }

    // 设置共享内存文件大小
    if (ftruncate(fd, SHARED_MEM_SIZE) == -1) {
        perror("ftruncate");
        shm_unlink("/my_shared_memory");
        return 1;
    }

    // 将共享内存文件映射到进程的地址空间
    shared_mem = mmap(NULL, SHARED_MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_mem == MAP_FAILED) {
        perror("mmap");
        shm_unlink("/my_shared_memory");
        return 1;
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        shm_unlink("/my_shared_memory");
        return 1;
    } else if (pid == 0) {
        // 子进程:读取并打印共享内存中的数据
        printf("Child process: %s\n", (char*)shared_mem);
    } else {
        // 父进程:向共享内存写入数据
        strncpy((char*)shared_mem, message, strlen(message));
        wait(NULL);
    }

    // 解除映射和关闭共享内存文件
    munmap(shared_mem, SHARED_MEM_SIZE);
    close(fd);
    shm_unlink("/my_shared_memory");

    return 0;
}

在这个示例中,父进程和子进程通过共享内存进行通信。它们使用 shm_open() 函数创建一个共享内存文件,并使用 ftruncate() 设置文件大小。然后,父进程使用 mmap() 函数将共享内存文件映射到进程的地址空间中。父进程向共享内存写入消息,子进程从共享内存读取并打印消息。

请注意,在实际使用中,需要根据具体的需求和通信方式进行同步和协调,以确保数据的一致性和正确性。此示例仅为演示基本原理,并未包含详尽的错误处理和同步机制。

mmap非父子进程的通信

写端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>

struct student{
    int id;
    char name[256];
    int age;
};


void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    struct student stu={1,"xiaoming",18};
    struct student *p;
    int fd;
    fd=open("test_map",O_RDWR|O_CREAT|O_TRUNC,0644);
    if(fd==-1){
        sys_err("open error");
    }
    
    ftruncate(fd,sizeof(stu));
    
    p=mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p==MAP_FAILED){
        sys_err("mmap error");
    }
    
    while(1){
        memcpy(p,&stu,sizeof(stu));
        stu.id++;
        sleep(1);
    }
 
    munmap(p,sizeof(stu));
    close(fd);
	return 0;
}

读端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>

struct student{
    int id;
    char name[256];
    int age;
};


void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    struct student stu;
    struct student *p;
    int fd;
    fd=open("test_map",O_RDONLY);
    if(fd==-1){
        sys_err("open error");
    }
    
    p=mmap(NULL,sizeof(stu),PROT_READ,MAP_SHARED,fd,0);
    if(p==MAP_FAILED){
        sys_err("mmap error");
    }
    
    while(1){
        printf("id=%d, name=%s, age=%d\n",p->id,p->name,p->age);
        sleep(1);
    }
    munmap(p,sizeof(stu));
    close(fd);
	 return 0;
}

mmap匿名映射区

匿名映射是一种特殊类型的映射,它不关联文件,而是在进程的地址空间中创建一块匿名的映射区域。匿名映射区域在创建时没有关联的文件,因此不能通过文件进行持久化存储。它主要用于进程间的通信或作为临时的工作区域。只能用于父子进程,因为父子进程共享匿名映射区。

mmap() 函数中使用匿名映射时,文件描述符参数应该是 -1,并且传递 MAP_ANONYMOUSMAP_ANON 标志来指示创建匿名映射。

以下是一个简单的示例代码,演示如何使用匿名映射创建一块共享内存区域:

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHARED_MEM_SIZE 4096

int main() {
    void* shared_mem;

    // 创建匿名映射区域
    shared_mem = mmap(NULL, SHARED_MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared_mem == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // 在共享内存中写入数据
    int* data = (int*)shared_mem;
    *data = 123;

    // 在子进程中读取共享内存中的数据
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        int value = *data;
        printf("Child process: Value in shared memory: %d\n", value);
    } else {
        sleep(1);  // 等待子进程读取共享内存中的数据
    }

    // 解除映射
    munmap(shared_mem, SHARED_MEM_SIZE);

    return 0;
}

在这个示例中,首先通过 mmap() 函数创建了一块匿名映射区域,大小为 SHARED_MEM_SIZE 字节。然后,将数据存储在共享内存中,子进程读取并打印了共享内存中的数据。

需要注意的是,匿名映射区域在解除映射后会被操作系统回收,其中的数据将丢失。匿名映射主要用于进程间的临时通信或数据共享,不会持久化到磁盘上。

信号

信号概念

信号(Signal)是一种用于进程间通信和处理异步事件的机制。它是一种软件中断,用于通知进程发生了某种事件,如用户输入、硬件异常、操作系统事件等。

每个信号都有一个唯一的编号,表示不同的事件。常见的一些信号编号包括SIGINT(终止进程)、SIGTERM(终止请求)、SIGKILL(立即终止进程)等。可以使用kill命令向进程发送信号,也可以使用C语言中的函数(如kill()signal()等)来发送和处理信号。

当进程接收到信号时,可以采取不同的行为来处理该信号。默认情况下,进程可能会终止、继续执行、忽略信号或执行用户指定的信号处理函数。可以使用信号处理函数来自定义对特定信号的处理方式。所有信号的产生及其处理全部都是由内核完成的
在这里插入图片描述

通过使用信号,进程可以与其他进程、操作系统和外部事件进行通信和交互。信号通常用于实现进程间的同步、异常处理和进程控制等功能。然而,需要小心处理信号,确保适当地处理和处理不同信号,以避免出现竞态条件或不确定的行为。

阻塞信号集和未决信号集
在Linux中,每个进程都有两个与信号相关的集合:阻塞信号集(Blocked Signal Set)和未决信号集(Pending Signal Set)。

  1. 阻塞信号集(Blocked Signal Set):阻塞信号集是一组信号,进程可以将其设置为阻塞状态,以防止接收到这些信号。当信号被阻塞时,进程仍然可以接收到信号,但信号将被排队,直到解除阻塞才会被递送给进程。进程可以使用 sigprocmask() 函数来修改阻塞信号集。(还没到进程

  2. 未决信号集(Pending Signal Set):未决信号集是一组当前被阻塞的信号,已经被发送到进程但尚未被处理的信号。当进程解除对某个信号的阻塞时,如果该信号在未决信号集中等待,则该信号将立即递送给进程。进程可以使用 sigpending() 函数来获取当前的未决信号集。(到了进程还未被执行
    在这里插入图片描述

这两个信号集的概念常用于处理信号的阻塞和处理过程中。通过设置阻塞信号集,进程可以选择性地阻塞某些信号,以便在适当的时候处理它们。而未决信号集则用于记录已经发送但还未处理的信号,进程可以查询该集合以了解当前等待处理的信号。

需要注意的是,当信号被阻塞时,它们不会丢失,而是在解除阻塞后递送给进程。进程应该小心处理阻塞和未决信号集,以确保正确处理和响应不同的信号事件。

常见信号:

kill -l
在这里插入图片描述

函数

  1. kill函数和kill命令
  2. alarm函数
    定时发送SIGALRM给当前进程,返回值是上次定时剩余时间。
unsigned int alarm(unsigned int seconds);

一秒内能打印多少数字:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc,char *arg[])
{
    int i;
    alarm(1);
    for(i=0; ;i++)
    {
        printf("%d\n",i);
    }
  
	  return 0;
}
  1. setitimer函数
    设置定时器(闹钟)。可替代alarm函数。精度微妙,可以实现周期定时。

信号集操作函数

在 C 语言的标准库 <signal.h> 中,提供了一些用于操作信号集(Signal Set)的函数,可以用于创建、修改和操作信号集。以下是常用的信号集操作函数:

  1. int sigemptyset(sigset_t *set)

    • 该函数用于将信号集 set 清空,即将所有的信号从集合中移除。
  2. int sigfillset(sigset_t *set)

    • 该函数用于将信号集 set 填满,即将所有的信号添加到集合中。
  3. int sigaddset(sigset_t *set, int signum)

    • 该函数将指定的信号 signum 添加到信号集 set 中。
  4. int sigdelset(sigset_t *set, int signum)

    • 该函数将指定的信号 signum 从信号集 set 中移除。
  5. int sigismember(const sigset_t *set, int signum)

    • 该函数用于检查指定的信号 signum 是否在信号集 set 中。如果在集合中,返回非零值;否则返回零。

这些函数使用 sigset_t 类型的变量来表示信号集,sigset_t 是一个位图,每一位对应一个信号编号,标记该信号是否在集合中。

以下是一个示例用法:

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

int main() {
    sigset_t myset;

    // 清空信号集
    sigemptyset(&myset);

    // 添加一些信号到信号集中
    sigaddset(&myset, SIGINT);
    sigaddset(&myset, SIGTERM);

    // 检查信号集中是否包含某个信号
    if (sigismember(&myset, SIGINT)) {
        printf("SIGINT is in the set\n");
    } else {
        printf("SIGINT is not in the set\n");
    }

    return 0;
}

上述示例演示了信号集操作函数的基本用法。首先使用 sigemptyset 清空信号集,然后使用 sigaddsetSIGINTSIGTERM 信号添加到集合中。最后使用 sigismember 检查 SIGINT 是否在信号集中,并相应地输出结果。

这些函数可以帮助我们创建、修改和查询信号集,以便进行信号的管理和处理。

sigprocmask函数
sigprocmask 函数是一个用于操作信号屏蔽字(Signal Mask)的函数,它用于设置或获取当前进程的信号屏蔽状态。

函数原型如下:

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how 参数指定了信号屏蔽字的操作方式,可以取以下几个值:

    • SIG_BLOCK:将 set 指定的信号集添加到当前进程的信号屏蔽字中。
    • SIG_UNBLOCK:从当前进程的信号屏蔽字中解除 set 指定的信号集的屏蔽。
    • SIG_SETMASK:将当前进程的信号屏蔽字设置为 set 指定的信号集。
  • set 参数是一个指向 sigset_t 类型的指针,用于指定要添加、解除或设置的信号集。

  • oldset 参数是一个指向 sigset_t 类型的指针,用于存储之前的信号屏蔽字。如果不需要保存旧的信号屏蔽字,可以将其设置为 NULL

函数的作用是根据 how 参数的指示来修改进程的信号屏蔽字,并将之前的信号屏蔽字保存到 oldset 中。通过设置信号屏蔽字,进程可以控制哪些信号在特定时间内被阻塞或接收。

示例用法:

#include <signal.h>

int main() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);

    // 将 SIGINT 信号添加到进程的信号屏蔽字中
    sigprocmask(SIG_BLOCK, &mask, NULL);

    // 在这里执行一些需要屏蔽 SIGINT 信号的操作

    // 解除对 SIGINT 信号的屏蔽
    sigprocmask(SIG_UNBLOCK, &mask, NULL);

    return 0;
}

上述示例中,sigprocmask 函数被用来在执行某些操作期间屏蔽 SIGINT 信号,以防止其中断当前进程的执行。操作完成后,使用 SIG_UNBLOCK 解除对 SIGINT 信号的屏蔽,使其能够再次接收到该信号。

sigpending 函数用于获取当前被阻塞且等待处理的未决信号集(Pending Signal Set)。

函数原型如下:

#include <signal.h>

int sigpending(sigset_t *set);
  • set 参数是一个指向 sigset_t 类型的指针,用于存储当前被阻塞且等待处理的未决信号集。

函数的作用是将当前进程中被阻塞且等待处理的未决信号集存储在 set 指向的内存中。未决信号集表示进程收到但由于被阻塞而暂时无法处理的信号。

示例用法:

#include <signal.h>

int main() {
    sigset_t pending_set;
    sigpending(&pending_set);

    // 检查未决信号集中是否包含特定信号,如 SIGINT
    if (sigismember(&pending_set, SIGINT)) {
        printf("SIGINT is pending\n");
    } else {
        printf("SIGINT is not pending\n");
    }

    return 0;
}

上述示例中,sigpending 函数用于获取当前进程的未决信号集,并将其存储在 pending_set 变量中。然后使用 sigismember 函数检查未决信号集中是否包含特定信号,如 SIGINT,并相应地输出结果。

注意,sigpending 函数只能获取当前被阻塞的未决信号集,而不能获取未被阻塞的信号集。要获取进程的当前信号屏蔽字,可以使用 sigprocmask 函数。

设置屏蔽字并进行打印,这段代码是一个简单的信号处理程序,用于阻塞 SIGINT 和 SIGQUIT 信号,并在程序运行期间打印当前信号集。以下是代码的详细解释:

  1. 包含头文件:代码中包含了多个头文件,分别是 stdio.h、stdlib.h、string.h、unistd.h、pthread.h、sys/mman.h、fcntl.h 和 signal.h,这些头文件分别提供了程序所需要的系统调用、标准库函数、字符串操作、系统数据结构、线程、内存管理和信号处理等功能。
  2. 错误处理函数:sys_err() 函数用于处理程序中的错误情况。当程序遇到错误时,它会调用 perror() 函数输出错误信息,然后调用 exit() 函数终止程序。
  3. 打印信号集函数:print_set() 函数用于打印当前信号集。它接收一个 sigset_t 类型的参数,表示当前信号集。函数中使用循环遍历信号集中的每个信号,如果信号在信号集中,则输出 1,否则输出 0。最后,函数使用 printf() 函数换行。
  4. 主函数:main() 函数是程序的入口点。它首先为信号集 set 初始化,使用 sigemptyset() 函数清空信号集,然后使用 sigaddset() 函数将 SIGINT 和 SIGQUIT 信号添加到信号集中。接着,它调用 sigprocmask() 函数,使用 SIG_BLOCK 标志将信号集设置为阻塞状态。如果设置成功,则返回 0;否则,调用 sys_err() 函数输出错误信息并终止程序。
  5. 信号处理循环:在主函数中,程序使用 while 循环不断地等待信号。每次循环,它首先调用 sigpending() 函数获取当前信号集,然后调用 print_set() 函数打印信号集。如果 sigpending() 函数调用失败,则调用 sys_err() 函数输出错误信息并终止程序。在每次循环结束后,程序使用 sleep() 函数暂停 1 秒,然后继续下一次循环,直到程序结束。:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <signal.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void print_set(sigset_t *set)
{
    int i;
    for(i=1;i<32;i++)
    {
        if(sigismember(set,i))
        {
            putchar('1');
        }
        else
        {
            putchar('0');
        }
    }
    printf("\n");
}

int main(int argc,char *arg[])
{
    sigset_t set,oldset,pedset;
    
    int ret=0;
    
    sigemptyset(&set);
    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGQUIT);
    
    ret=sigprocmask(SIG_BLOCK,&set,&oldset);
    if (ret==-1)
        sys_err("sigprocmask error");
        
    while(1){
        ret=sigpending(&pedset);
        if (ret==-1)
            sys_err("sigpending error");
            
        print_set(&pedset);
        sleep(1);
    }
  
	  return 0;
}

信号捕捉

signal 函数是 C 语言中用于设置信号处理函数的函数,主要用于在程序运行过程中处理外部信号。该函数的原型为:

void signal(int signo, void (*handler)(int));

其中,signo 是信号编号,handler 是信号处理函数指针。
在 signal 函数中,可以使用信号编号和信号处理函数指针作为参数来指定信号处理函数。信号编号是一个整数,用于标识要处理的信号。信号处理函数指针是一个指向函数的指针,该函数用于处理信号。
使用 signal 函数可以设置信号处理函数,以便在程序运行期间处理外部信号。例如,可以使用 signal 函数来设置一个信号处理函数,以便在程序接收到 SIGINT 信号时执行该函数。下面是一个简单的示例:

#include <stdio.h>
#include <signal.h>
void my_signal_handler(int sig)
{
   printf("Signal received\n");
}
int main()
{
   signal(SIGINT, my_signal_handler);
   while(1)
   {
       // do something
   }
   return 0;
}

在这个示例中,我们定义了一个名为 my_signal_handler 的信号处理函数,该函数接受一个整数参数 sig,用于表示接收到的信号编号。然后,我们使用 signal 函数将 my_signal_handler 函数设置为 SIGINT 信号的处理函数。最后,在程序循环中,我们使用 while 循环等待信号的到来,并在信号到来时执行 my_signal_handler 函数。
在 C 语言中,还可以使用 sigaction 函数来设置信号处理函数,该函数的原型为:

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

其中,signo 是信号编号,act 是信号处理函数指针,oldact 是旧信号处理函数指针。
使用 sigaction 函数可以设置信号处理函数,以便在程序运行期间处理外部信号。例如,可以使用 sigaction 函数来设置一个信号处理函数,以便在程序接收到 SIGINT 信号时执行该函数。下面是一个简单的示例:

#include <stdio.h>
#include <signal.h>
void my_signal_handler(int sig)
{
   printf("Signal received\n");
}
int main()
{
   struct sigaction act;
   act.sa_handler = my_signal_handler;
   act.sa_flags = 0;
   act.sa_restorer = NULL;
   if(sigaction(SIGINT, &act, NULL) < 0)
   {
       printf("sigaction error\n");
   }
   while(1)
   {
       // do something
   }
   return 0;
}

在这个示例中,我们定义了一个名为 my_signal_handler 的信号处理函数,该函数接受一个整数参数 sig,用于表示接收到的信号编号。然后,我们使用 sigaction 函数将 my_signal_handler 函数设置为 SIGINT 信号的处理函数。最后,在程序循环中,我们使用 while 循环等待信号的到来,并在信号到来时执行 my_signal_handler 函数。

内核实现信号捕捉过程

在这里插入图片描述

sigchild信号回收子进程

信号回收子进程(sigchild)是指在进程间通信中,一个进程(子进程)向另一个进程(父进程)发送一个信号,表示子进程已经完成任务并请求父进程回收其资源。这个过程通常用于进程管理和进程间同步。
以下是 sigchild 信号回收子进程的函数原型:

void sigchild(int sig, siginfo_t *siginfo, void *ucontext);

其中:

  • sig:信号编号,用于标识发送的信号。
  • siginfo:信号信息结构体,包含信号的相关信息。
  • ucontext:上下文环境指针,用于传递信号处理函数的额外数据。
    以下是一个简单的代码举例,演示如何使用 sigchild 函数实现信号回收子进程:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void my_signal_handler(int sig, siginfo_t *siginfo, void *ucontext) {
   printf("Child process has finished its task\n");
   // 回收子进程资源
   // ...
}
int main() {
   signal(SIGCHLD, my_signal_handler);
   // 创建子进程
   // ...
   waitpid(-1, NULL, 0); // 等待子进程完成
   return 0;
}

在这个例子中,我们首先定义了一个名为 my_signal_handler 的信号处理函数,它接受三个参数:信号编号、信号信息结构体和上下文环境指针。当子进程完成任务时,它会向父进程发送 SIGCHLD 信号。父进程在接收到这个信号后,会调用 my_signal_handler 函数来处理信号。在信号处理函数中,我们打印一条消息表示子进程已经完成任务,然后进行资源回收等工作。
请注意,这里的代码举例仅供参考,实际应用中可能需要根据具体需求进行修改。

最终示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void catch_child(int signo){   //有子进程终止,发送SIGCHLD信号时,该函数会被内核回调
    //pid_t wpid;
    //while((wpid=wait(NULL))!=-1){
    //    if (wpid==-1){
    //        sys_err("wait error");
    //    }
    //    printf("------catch child id %d\n",wpid);
    //}
    
    pid_t wpid;
    int status;
    
    while((wpid=waitpid(-1,&status,0))!=-1){  //循环回收防止僵尸进程出现
    
        if(WIFEXITED(status)){
            printf("------catch child id %d , ret=%d\n",wpid,WEXITSTATUS(status));
        }
    }
    
    return;
}

int main(int argc,char *arg[])
{
    pid_t pid;
    
    //阻塞
    sigset_t set;
    sigemptysetz(&set);
    sigaddset(&set,SIGCHLD);
    
    sigprocmask(SIG_BLOCK,&set,NULL);
    
    int i;
    for(i=0;i<15;i++){
        if((pid=fork())==0)             //创建多个子进程
            break;
    }
    
    if(i==15){
        struct sigaction act;
        act.sa_handler=catch_child;     //设置回调函数
        sigemptyset(&act.sa_mask);      //设置捕捉函数执行期间屏蔽字
        act.sa_flags=0;                 //设置默认属性,本信号自动屏蔽
        sigaction(SIGCHLD,&act,NULL);   //注册信号捕捉函数
        
        //解除阻塞
        //子进程死后会发送信号,如若wait未来得及注册会出现僵尸进程,此时设置屏蔽字
        //子进程死后先把信号阻塞,等待wait注册完,就能接受信号。
        sigprocmask(SIG_UNBLOCK,&set,NULL);
        
        printf("I am parent %d\n",getpid());
        while(1);
    }else{
        //sleep(1);
        printf("I am child pid=%d\n",getpid());
        return i;
    }
  
	  return 0;
}

中断系统调用

在这里插入图片描述

进程组和会话

进程组和会话是Linux系统中的两个重要概念。进程组是一个或多个进程的集合,而会话是一个或多个进程组的集合。一个或多个进程的集合属于一个进程组,而一个或多个进程组属于一个会话。

创建会话时需要注意以下几点:

  • 会话ID(Session ID):每个会话都有一个唯一的ID,可以用来标识该会话。
  • 控制终端:在拥有控制终端的会话中,session leader也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的shell进程(login shell)。
  • 前台进程组:在拥有控制终端的会话中,只有一个前台进程组,只有前台进程组中的进程才可以和控制终端进行交互。其他进程组都是后台工作(background)。
    在这里插入图片描述

守护进程
守护进程是一种运行在后台的特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。 在Linux系统中,守护进程通常是由init进程启动的,而init进程是由内核启动的第一个进程。不受用户登陆注销影响
创建步骤:
1、fork子进程,让父进程终止
2、子进程调用setsid()创建新会话
3、通常根据需要,改变工作目录位置chdir()
4、通常根据需要,重设umask文件掩码权限
5、通常根据需要,关闭/重定向文件描述符
6、守护进程业务逻辑。while()

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <signal.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}



int main(int argc,char *arg[])
{
    pid_t pid;
    int ret,fd;
    
    pid=fork();
    
    if(pid>0)  //终止父进程
        exit(0);
        
    pid=setsid();  //创建新会话
    if(pid==-1)
        sys_err("setsid error");
        
    ret=chdir("/home/kewang/data/daemon");  //改变工作目录位置
    if(ret==-1)
        sys_err("chdir error");
        
    umask(0022);  //改变文件访问权限掩码
    
    close(STDIN_FILENO);  //关闭文件描述符 0
    fd=open("/dev/null",O_RDWR); //fd->0
    if (fd==-1)
        sys_err("open error");
        
    dup2(fd,STDOUT_FILENO); //重定向stdout和stderr
    dup2(fd,STDERR_FILENO);
    
    while(1);  //模拟守护进程业务
  
	  return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值