进程线程间的通信

进程和线程之间有很多种方法进行通信,如下是需要掌握的通信方式:

  • 无名管道(pipe)
  • 有名管道(fifo)
  • 信号(signal)
  • 共享内存(mmap)

本文章代码存放在GitHub中的UNIX_Coding中,需要自行查看

https://github.com/Scholar618/UNIX_Codings/tree/main/Linux

无名管道

定义:在内核中开辟一块内存作为通道连接两个进程之间的通信,它一般是单向的

进程线程间的通信_第1张图片

特点 :

  1. 只能在具有公共祖先的两个进程之间使用
  2. 单工(单向)的通信模式,具有固定的读写端
  3. 无名管道创建时会返回两个文件描述符,分别用于读写管道

无名管道创建

int pipe(int pfd[2]);
  • 成功时返回0,失败时返回EOF
  • pfd包含两个元素的整形数组,用来保存文件描述符
  • pfd[0]用于读管道,pfd[1]用于写管道

示例程序:

#include 
#include 
#include 
int main() {
	int pfd[2];
	int re;
	char buf[20];
	pid_t pid;
	re = pipe(pfd);
	if(re < 0) {
		perror("pipe");
		return 0;
	}
	pid = fork();
	if(pid < 0) {
		perror("fork");
		return 0;
	} else if(pid == 0) { // 子进程
		while(1) {
			strcpy(buf, "I love Linux!");
			write(pfd[1], buf, strlen(buf));
			sleep(1);
		}
	} else { // 父进程	
		while(1) {
			re = read(pfd[0], buf, 20);
			if(re > 0) {
				printf("read pipe = %s\n", buf);
			}
		}
	}
	return 0;
}

进程线程间的通信_第2张图片

管道可以由大于等于两个进程共享

例如一个父进程进行读数据,两个子进程进行写数据
示例程序:

#include 
#include 
#include 
int main(){
    int pfd[2];
    int i;
    int re;
    char buf[40]={0};
    pid_t pid;
    re = pipe(pfd);
    if(re<0){
        perror("pipe");
        return 0;
    }
    printf("%d,%d\n",pfd[0],pfd[1]);
 
    for(i=0;i<2;i++){
        pid = fork();
        if(pid<0){
            perror("fork");
            return 0;
        }else if(pid>0){
 
        }else{
            break;   
             
        }
    }
    if(i==2){
        close(pfd[1]);
        while(1){
            memset(buf,0,40);
            re=read(pfd[0],buf,40);
            if(re>0){
                printf("%s\n",buf);
            }    
        }
        return 0;
    }
 
    if(i==1){
        close(pfd[0]);
        while(1){
            strcpy(buf,"this is 2 process");
            write(pfd[1],buf,strlen(buf));
            usleep(930000);
        }
        return 0;
    }
    if(i==0){
        close(pfd[0]);
        while(1){
            strcpy(buf,"this is 1 process");
            write(pfd[1],buf,strlen(buf));
            sleep(1);
        }
 
        return 0;
    }
 
 
}

无名管道的读写特性

读管道:

  1. 管道中有数据,read返回实际读到的字节数
  2. 管道中无数据:
    1. 管道写端被全部关闭,read返回0(假设读到文件末尾)
    2. 写段没有被完全关闭,read阻塞等待(以后可能会有数据传递,此时让出CPU)

写管道:

  1. 管道读端全部被关闭,进程异常终止(也可以捕捉SIGPIPE信号,使进程不终止)
  2. 管道读端没有全部关闭:
    1. 管道已满,write阻塞(管道大小64K)
    2. 管道未满,write将数据写入,并返回实际写入的字节数

有名管道

特点

  • 有名管道可以使非亲缘关系的两个进程相互通信
  • 通过路径名来操作,在文件系统中可见,但内容存放在内存中
  • 文件IO来操作有名管道
  • 遵循先进先出原则
  • 不支持leek操作
  • 单工读写

有名管道的创建

#include 
#include 

int mkfifo(const char *path, mode_t mode);
  • 成功时返回0,失败时返回EOF
  • path创建的管道文件路径
  • mode管道文件的权限,如0666

示例代码:fifor.c(读管道)

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main() {
	int re;
	int fd;
	char buf[32];
	fd = open("/myfifo", O_RDONLY);
	if(fd < 0) {
		perror("open");
		return 0;
	}
	while(1) {
		re = read(fd, buf, 32);
		if(re > 0) {
			printf("read fifo = %s",buf);
		} else if(re == 0) {
			exit(0);
		}
	}
}

fifow.c(写管道)

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main() {
	int re;
	int fd;
	char buf[32];
	re = mkfifo("/myfifo", 0666);
	if(re < 0) {
		perror("mkfifo");
//		return 0;
	}
	fd = open("/myfifo", O_WRONLY|O_NONBLOCK);
	if(fd < 0) {
		perror("open");
		return 0;
	}
	printf("after open!\n");
	while(1) {
		fgets(buf, 32, stdin);
		write(fd, buf, strlen(buf));
	}

}

有名管道注意事项:

  1. 程序不能以O_RDWR(读写)模式打开FIFO文件进行读写操作
  2. 第二个参数中的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,如果没有这个选项为阻塞的

共享内存

概念:

  • 共享内存也叫内存映射
  • 共享内存可以使用mmap()映射普通文件
  • 使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像普通内存那样对文件进行访问,不必再调用write,read

进程线程间的通信_第3张图片

函数定义:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);	
  • 功能:创建共享内存映射
  • 函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED( ((void *) -1) ),设置errno值
  • 参数:
    • addr:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
    • length:必须>0。映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。

    • prot:指定共享内存的访问权限。可取如下几个值的可选:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)。

    • flags:由以下几个常值指定:MAP_SHARED(共享的) MAP_PRIVATE(私有的), MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。MAP_ANONYMOUS(匿名映射,用于血缘关系进程间通信)

    • fd:表示要映射的文件句柄。如果匿名映射写-1。

    • offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。

注意事项:

(1) 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。

(2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护),如果不满足报非法参数(Invalid argument)错误。

当MAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享。

(3) 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。

(4) 用于映射的文件大小必须>0,当映射文件大小为0时,指定非0大小创建映射区,访问映射地址会报总线错误,指定0大小创建映射区,报非法参数错误(Invalid argument)

(5) 文件偏移量必须为0或者4K的整数倍(不是会报非法参数Invalid argument错误).

(6)映射大小可以大于文件大小,但只能访问文件page的内存地址,否则报总线错误 ,超出映射的内存大小报段错误

信号

信号是软件中断,是一种处理异步事件的方法,例如终端用户输入中断键,会通过信号机制停止程序。例如kill -9 程序号,等等都是信号机制。

概念:

使用kill -l命令查看所有信号编号,注意哦,不存在编号为0的信号。编号为0的信号有新的用途。

进程线程间的通信_第4张图片

一些常用信号解释:

进程线程间的通信_第5张图片

 产生信号的方式: 

  • 用户按下某些终端键时,引发终端产生的信号,例如(Ctrl + C)产生中断信号(SIGNALINT)
  • 硬件异常产生信号:除数为0或者无效的内存引用等。会有硬件检测出来,并通知内核,然后内核为该进程产生信号。例如:对执行一个无效内存引用的进程产生(SIGSEGV)信号
  • 进程调用kill函数将任意信号发送给另一个进程或进程组,但是,接收信号进程必须和发送信号进程的所有者完全相同。或者发送信号进程是root用户
  • 用户使用kill命令
  • 当检测到某种软件条件已经发生,并通知有关进程时也会产生信号。

处理信号的方式:

  • 忽略信号:两种信号不能被忽略,SIGKILL(9)和SIGSTOP(19)
  • 捕捉信号:在用户函数中,执行用户希望对这种事件进行的处理。
  • 执行系统默认动作:如上图,执行默认动作,注意,对大多数信号的系统默认动作是终止该进程。

函数定义

#include 

// 1.
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

// 2.
void (*signal(int signo, void (*func) (int))) (int);

signo是信号名, func值常量是SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数地址

  • SIG_IGN:向内核表示忽略此信号(SIGKILL和SIGSTOP)不能被忽略
  • SIG_DFL:表示接到此信号后的动作是系统默认动作。
  • 函数地址:信号发生时,调用该函数,称为捕捉信号,称函数为信号处理程序、信号捕捉函数。

#include 头文件中,有如下定义:

#define SIG_ERR (void(*)())-1   // 指定一个无效的处理程序
#define SIG_DFL (void(*)())0    // 使用默认的处理方式
#define SIG_IGN (void(*)())1    // 忽略信号

共享内存

允许两个或多个进程共享一个给定的存储区,在多个进程之间同步访问一个给定的存储区

函数定义

#include 

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

注意:共享内存实现进程间通信是进程间通信最快的。

你可能感兴趣的:(Linux学习,linux,c语言,unix)