IO【6】(条件变量和进程间通信)

242 阅读13分钟

​ “这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

条件变量

条件变量是为了完成线程间同步指定出来的一种机制,是利用将一个线程挂起等待。然后由另一方发送一个条件成立的信号将其唤醒继续运行的原理。

缺点:等待一方必须要先获取到锁

图片.png

函数接口

1.静态初始化条件变量

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2.动态初始化条件变量

  1. 原型:

    int pthread_cond_init(pthread_cond_t * cond,    pthread_condattr_t *cond_attr); 
    
  2. 参数:

    1. cond :条件变量的地址
    2. cond_attr:使用缺省模式NULL
  3. 返回值:

    1. 成功返回0
    2. 失败返回非0值

3.thread_cond_wait

  1. 原型:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

  2. 功能:主动挂起等待信号的带来

  3. 参数:

    1. cond :条件变量的地址
    2. mutex :锁变量的地址
  4. 注意:wait函数和一把锁相绑定, 当cond——wait函数运行后,

    ​ 1.先进行解锁操作,让出这把锁的使用权

    ​ 2.然后将线程挂起

    ​ 3.当信号到来时,准备运行之前,需要先尝试加锁

    ​ 1.2两步骤属于原子操作

  5. 返回值:

    1. 成功返回0
    2. 失败返回非0值

4.pthread_cond_signal

  1. 原型:int pthread_cond_signal(pthread_cond_t *cond);
  2. 功能:发送一个条件成立的信号,一次性的,每次只能唤醒一个线程,谁收到谁动
  3. 参数:条件变量的地址
  4. 返回值:
    1. 成功返回0
    2. 失败返回非0值

5.pthread_cond_broadcast

  1. 原型: int pthread_cond_broadcast(pthread_cond_t *cond);
  2. 功能:发送一个条件成立的信号,唤醒所有等待的线程,一个广播信号
  3. 参数:条件变量的地址
  4. 返回值:
    1. 成功返回0
    2. 失败返回非0值

6.pthread_cond_destroy

  1. 原型:int pthread_cond_destroy(pthread_cond_t *cond);
  2. 功能:销毁一个条件变量
  3. 参数:条件变量的地址
  4. 返回值:
    1. 成功返回0
    2. 失败返回非0值

7.pthread_cond_timedwait

原型:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);        

代码:一对兄弟,都没有考试及格,妈妈回家后开始训话的一个场景。

这里特别要注意,不仅兄弟那里要上锁解锁,同时妈妈那里也要上锁。

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

//定义一把锁,一个条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond1 = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

//myfun1作为哥哥
void * myfun1(void * arg)
{
    printf("哥哥回到家先将书包放回房间\n");
    printf("哥哥来到妈妈面前\n");
    //等待妈妈唤醒我,我在回答问题
    while(1)
    {
        pthread_mutex_lock(&mutex); //上锁
        pthread_cond_wait(&cond,&mutex);//解锁,挂起,上锁
        //哥哥要说的话
        pthread_mutex_lock(&mutex2);//哥哥和弟弟
        printf("哥哥回答\n");
        sleep(3);
        printf("哥哥回答完毕\n");
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex); //解锁
    }
}

//myfun2做为弟弟
void * myfun2(void * arg)
{
    printf("弟弟回到家先将书包放回房间\n");
    printf("弟弟来到妈妈面前\n");
    //等待妈妈唤醒我,我在回答问题
    while(1)
    {
        pthread_mutex_lock(&mutex1); //上锁
        pthread_cond_wait(&cond1,&mutex1);//解锁,挂起,上锁
        //弟弟要说的话
        pthread_mutex_lock(&mutex2);
        printf("弟弟回答\n");
        sleep(3);
        printf("弟弟回答完毕\n");
        pthread_mutex_unlock(&mutex2);

        pthread_mutex_unlock(&mutex1); //解锁
    }

}
int main(int argc, const char *argv[])
{
    //创建线程--二个
    pthread_t tid[2];
    if(0 != pthread_create(&tid[0],NULL,myfun1,NULL))
    {
        perror("create1");
        return -1;
    }
    if(0 != pthread_create(&tid[1],NULL,myfun2,NULL))
    {
        perror("create2");
        return -1;
    }
    //主线程
    printf("弟弟和哥哥回来了,妈妈准备开始训话\n");
    printf("哥哥和弟弟都来到了面前,等待被唤醒\n");
   

    while(1)
    {
        char a = 0;//1代表叫哥哥 ,2 代表叫弟弟
        scanf("%c",&a);
        getchar();
        switch (a)
        {
        case '1':
            //唤醒哥哥
            pthread_mutex_lock(&mutex);
            pthread_cond_signal(&cond);//说了一句哥哥回答
            pthread_mutex_unlock(&mutex);
		 	//pthread_cond_broadcast(&cond);
            break;
        case '2':
            //唤醒弟弟
            pthread_mutex_lock(&mutex1);
            pthread_cond_signal(&cond1);//说了一句哥哥回答
            pthread_mutex_unlock(&mutex1);

            break;
        default:
            break;
        }
    }
    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);
    return 0;
}
linux@ubuntu:~/demo/test/IO/test$ gcc test.c -lpthread
linux@ubuntu:~/demo/test/IO/test$ ./a.out 
弟弟和哥哥回来了,妈妈准备开始训话
哥哥和弟弟都来到了面前,等待被唤醒
弟弟回到家先将书包放回房间
弟弟来到妈妈面前
哥哥回到家先将书包放回房间
哥哥来到妈妈面前
1
哥哥回答
2
2
2
2
2
哥哥回答完毕
弟弟回答
弟弟回答完毕
弟弟回答
弟弟回答完毕//只会有两次
//去掉了妈妈的锁之后
linux@ubuntu:~/demo/test/IO/test$ ./a.out 
弟弟和哥哥回来了,妈妈准备开始训话
哥哥和弟弟都来到了面前,等待被唤醒
弟弟回到家先将书包放回房间
弟弟来到妈妈面前
哥哥回到家先将书包放回房间
哥哥来到妈妈面前
1
哥哥回答
2
2
2
2
2
哥哥回答完毕
弟弟回答
弟弟回答完毕
1
哥哥回答
2
1
2
1
哥哥回答完毕
弟弟回答
弟弟回答完毕

linux@ubuntu:~/demo/test/IO/test$ ./a.out 
弟弟和哥哥回来了,妈妈准备开始训话
哥哥和弟弟都来到了面前,等待被唤醒
弟弟回到家先将书包放回房间
弟弟来到妈妈面前
哥哥回到家先将书包放回房间
哥哥来到妈妈面前
1
哥哥回答
2
1
2
1
2
哥哥回答完毕
弟弟回答
弟弟回答完毕
弟弟回答
弟弟回答完毕
哥哥回答
哥哥回答完毕
弟弟回答
弟弟回答完毕
哥哥回答
哥哥回答完毕

进程间通信

主要是利用内核空间,来完成两个进程或者多个进程之间的资源和信息的传递。

进程间通信方式:(7大类)

1.传统通信方式:

1.无名管道 使用的队列

2.有名管道 使用的队列

3.信号 异步的方式

2.IPC通信方式(第五代操作系统):

1.消息队列 管道的集合

2.共享内存 地址映射的方式

3.信号灯集 信号灯的集合

3.网络通信

套接字 :socket

传统通信方式之无名管道

📃附加:

单工通信方式:任何时间点,只能由一方发给一方,方向不允许改变。

半双工通信方式:同一时间内,只允许有一方发给一方,具有双方通信的能力。

全双工通信方式:任意时间点,双方任意可以给对方发送信息。

无名管道介绍:

无名管道是是实现(亲缘间)进程通信的一种方式,属于半双工通信方式,类似于一个水管。只有两个端,一个是数据流入端(写端),数据流出端(读端)。这两个端都是固定的端口。遵循数据的先进先出,数据拿出后就消失。管道是有限长度的64*1024个字节。无名管道,不在文件系统上体现,数据存在内存之上,进程结束以后。数据就丢失。管道文件不能使用lseek读写指针偏移。

无名管道原理图:

图片.png

创建一个无名管道

  1. 头文件:#include <unistd.h>
  2. 原型:int pipe(int pipefd[2]);
  3. 功能:创建一个无名管道,会将读写端两个文件描述符分别封装到fd[0]和fd[1]
    1. fd[0] ----r
    2. fd[1] ----w
  4. 返回值:
    1. 成功返回 0 ;
    2. 失败返回 -1;

管道注意点:

1.如果管道中没有数据,read读取时会阻塞等待数据的到来

//第一个注意点,管道没数据进行读取时会如何
char buf[123] = {0};
ssize_t ret = read(fd[0],buf,sizeof(buf));
if(-1 == ret)
{
	perror("read");
	return -1;
}
printf("读到的数据为%s\n",buf);

2.管道符合先进先出的原则,数据读走后就会消失

write(fd[1],"hello world",11);
char buf[123] = {0};
ssize_t ret = read(fd[0],buf,5);
if(-1 == ret)
{
	perror("read");
	return -1;
}
printf("读到的数据为%s\n",buf);
read(fd[0],buf,6);
printf("读到的数据为%s\n",buf);        

3.管道的大小是64K,管道写满以后再次进行写入会阻塞等待写入

防止有效数据丢失

int i = 0;
char ch;
for ( i = 0; i < 64*1024; i++)
{
	write(fd[1],&ch,1);
}
printf("管道已经写满\n");
write(fd[1],&ch,1);//及确定管道的大小
//又确定了管道写满之后再次写入会发生什么

4.如果关闭了写入端口,读会发生什么情况

  1. 管道中有数据时将里面的数据读出来。
  2. 管道中无数据时管道机制会认为写端关闭,不会再有数据到来,read在做读取时阻塞没有任何用处,read将不会阻塞等待了。便不会影响进程运行

代码:

char buf[123] = {0};
char buf1[123] = {0};
write(fd[1],"hello world",11);
close(fd[1]);//关闭写端
read(fd[0],buf,sizeof(buf));
printf("buf = %s\n",buf);
read(fd[0],buf1,sizeof(buf));//阻塞等待
printf("buf = %s\n",buf1);

5.如果读端关闭,在进行写入会发生“管道破裂”

因为:如果读端关闭,写入将没有任何意义了,并且每次调用write函数写入的数据都被称为有效数据。如果写入会造成有效数据的丢失,所以在写入时会出现管道破裂的问题结束进程。

close(fd[0]);//读端关闭
//开始写入数据
char ch;
write(fd[1],&ch,1);//写入失败了
//结束了
printf("写入成功\n");     

使用无名管道实现亲缘间进程通信

因为fork函数创建完子进程后,文件描述符也会被复制过去,相当于父子进程利于相同的文件描述符去操作一个文件指针,进而操作一个文件。

图片.png

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

int main(int argc, const char *argv[])
{
	//创建无名管道
	int fd0[2];
	int fd1[2];

	if(-1 == pipe(fd0)) //管道0用于父进程给子进程发送消息
	{
        perror("pipe");
    	return -1;
	}
	if(-1 == pipe(fd1))//管道1用于子进程给父进程发送消息
	{
    	perror("pipe");
    	return -1;
	}
	//创建进程
	pid_t pid = fork();
	if(-1 == pid)
	{
    	perror("fork");
    	return -1;
	}
	if(0 == pid)
	{
    	//关闭无名管道描述符
    	close(fd0[1]);
    	close(fd1[0]);
    	//子进程
    	while(1)
    	{	
        	char buf[123]={0};
        	read(fd0[0],buf,sizeof(buf));
        	if(strcmp(buf,"quit") == 0)
        	{
        	    printf("通话结束\n");
        	    exit(0);
        	}
        	printf("父进程说%s\n",buf);
		
        	//开始回复信息
        	printf("请子进程输入\n");
        	char buf1[123]={0}; //接收要发送的数据-->stdin
        	fgets(buf1,123,stdin); //必须去掉\n
        	buf1[strlen(buf1)-1] = '\0';
        	write(fd1[1],buf1,strlen(buf1));
        	if(strcmp(buf1,"quit") == 0)
        	{
        	    printf("通话结束\n");
        	    exit(0);
        	}
    	}
	}else if(pid > 0)
	{
    	//父进程
    	close(fd0[0]);
    	close(fd1[1]);
    	//发送消息
    	while(1)
    	{
        	printf("请父进程输入\n");
        	char buf[123]={0}; //接收要发送的数据-->stdin
        	fgets(buf,123,stdin); //必须去掉\n
        	buf[strlen(buf)-1] = '\0';
        	write(fd0[1],buf,strlen(buf));
        	if(strcmp(buf,"quit") == 0)
        	{
        	    printf("通话结束\n");
        	    wait(NULL);
        	    exit(0);
        	}
        	//开始接收子进程发送过来的数据
        	char buf1[123]={0};
        	read(fd1[0],buf1,sizeof(buf1));
        	printf("收到子进程发过来的数据为%s\n",buf1);
        	if(strcmp(buf1,"quit") == 0)
        	{
        	    printf("通话结束\n");
        	    wait(NULL);
        	    exit(0);
        	}     
    	} 
	}
	return 0;
}
  
linux@ubuntu:~/demo/test/IO/test$ ./a.out 
请父进程输入
吃了没?
父进程说吃了没?
请子进程输入
吃了
收到子进程发过来的数据为吃了
请父进程输入
吃啥了
父进程说吃啥了
请子进程输入
面
收到子进程发过来的数据为面
请父进程输入
quit
通话结束
通话结束

传统通信方式之有名管道

有名管道是建立无名管道基础上,为了完善无名管道只能用于亲缘间进程的这个缺点来延申出的一种进程间通信方式。继承无名管道的所有点。有名管道在文件系统中属于一种特殊的管道文件,虽然在文件系统上有所体现。但是它数据并不存放在磁盘之上,数据存在于内存之上,进程结束,数据就丢失了。

有名管道作为一个文件系统上的文件,如果实现非亲缘间进程通信的话,需要open打开这个文件,那么两个进程需要分别以读、写权限打开。如果打开有名管道的方式,不足读写这两个权限。open会阻塞等待另一个权限的到来。

创建有名管道

第一种方式:linux命令 mkfifo + 有名管道名字

第二种方式:c语言函数接口

  1. 头文件:
    1. #include <sys/types.h>
    2. #include <sys/stat.h>
  2. 原型:int mkfifo(const char *pathname, mode_t mode);
  3. 功能:创建一个有名管道
  4. 参数:
    1. pathname:目标路径及名称
    2. mode:权限 例如:0666
  5. 返回值:
    1. 成功返回0
    2. 失败返回-1;

代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
    //创建有名管道,不具备去检测文件存在则打开文件的功能
    if(-1 == mkfifo("./myfifo",0664))
    {
        if(errno == EEXIST)
        {
            printf("文件是存在的,直接打开就可以\n");
        }else{
            perror("mkfifo");
            return -1;
        }
    }
    //打开有名管道
    int fd = open("./myfifo",O_WRONLY);
    if(-1 == fd)
    {
        perror("open");
        return -1;
    }
    printf("打开文件成功\n");
    return 0;
}

使用有名管道来实现非亲缘间进程之间的通信

注意:要创建两个文件,会生成两个管道文件;在编译运行的时候,用两个窗口。可以实现那种对话的效果。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
    //创建有名管道,不具备去检测文件存在则打开文件的功能
    if(-1 == mkfifo("./myfifo",0664))
    {
        if(errno == EEXIST)
        {
            printf("文件是存在的,直接打开就可以\n");
        }else{
            perror("mkfifo");
            return -1;
        }
    }
    if(-1 == mkfifo("./myfifo1",0664))
    {
        if(errno == EEXIST)
        {
            printf("文件是存在的,直接打开就可以\n");
        }else{
            perror("mkfifo");
            return -1;
        }
    }
    //打开有名管道
    int fd = open("./myfifo",O_WRONLY);
    if(-1 == fd)
    {
        perror("open");
        return -1;
    }
    printf("打开文件成功\n");
    //打开有名管道
    int fd1 = open("./myfifo1",O_RDONLY);
    if(-1 == fd)
    {
        perror("open");
        return -1;
    }
    printf("打开文件成功\n");
	
    while(1)
    {
        printf("请父进程输入\n");
        char buf[123]={0}; //接收要发送的数据-->stdin
        fgets(buf,123,stdin); //必须去掉\n
        buf[strlen(buf)-1] = '\0';
        write(fd,buf,strlen(buf));
        if(strcmp(buf,"quit") == 0)
        {
            printf("通话结束\n");
            wait(NULL);
            exit(0);
        }
        //开始接收子进程发送过来的数据
        char buf1[123]={0};
        read(fd1,buf1,sizeof(buf1));
        printf("收到子进程发过来的数据为%s\n",buf1);
        if(strcmp(buf1,"quit") == 0)
        {
            printf("通话结束\n");
            wait(NULL);
            exit(0);
        }   
    } 	
	return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
    //创建有名管道,不具备去检测文件存在则打开文件的功能
    if(-1 == mkfifo("./myfifo",0664))
    {
        if(errno == EEXIST)
        {
            printf("文件是存在的,直接打开就可以\n");
        }else{
            perror("mkfifo");
            return -1;
        }
    }
    if(-1 == mkfifo("./myfifo1",0664))
    {
        if(errno == EEXIST)
        {
            printf("文件是存在的,直接打开就可以\n");
        }else{
            perror("mkfifo1");
            return -1;
        }
    }
    //打开有名管道
    int fd = open("./myfifo",O_RDONLY);
    if(-1 == fd)
    {
        perror("open");
        return -1;
    }
    printf("打开文件成功\n");
    int fd1 = open("./myfifo1",O_WRONLY);
    if(-1 == fd)
    {
        perror("open");
        return -1;
    }
    printf("打开文件成功\n");

    while(1)
    {
        char buf[123]={0};
        read(fd,buf,sizeof(buf));
        if(strcmp(buf,"quit") == 0)
        {
            printf("通话结束\n");
            exit(0);
        }
        printf("父进程说%s\n",buf);

        //开始回复信息
        printf("请子进程输入\n");
        char buf1[123]={0}; //接收要发送的数据-->stdin
        fgets(buf1,123,stdin); //必须去掉\n
        buf1[strlen(buf1)-  1] = '\0';
        write(fd1,buf1,strlen(buf1));
        if(strcmp(buf1,"quit") == 0)
        {
            printf("通话结束\n");
            exit(0);
        }
    }

    return 0;
}

传统通信方式之信号

信号是什么

信号层软件层对硬件层中断的一种模拟,是一个异步的信号。

中断:是一个优先级高的代码事件

linux所提供的信号

现阶段linux提供了64个信号

产看所有信号kill -l 一般 kill -9 ID

图片.png

图片.png

信号的原理

在进程创建初期,会在为进程创建一个信号函数表:

图片.png

信号的处理方式

  1. 忽略 :指的是信号到来了不采取任何措施 如:SIGCHLD

    SIGCHLD :子进程结束后给父进程发送的一个信号SIGKILL 和 SIGSTOP 不能被忽略。

  2. 捕捉 :指的是信号到来之前,将信号函数表中信号所对应的默认函数指针,修改成指向自己定义的函数---为我所用SIGKILL 和 SIGSTOP 不能被捕捉。

  3. 默认 :指的是信号到来以后,去执行进程创建初期信号函数表中的默认操作。

信号的相关函数

1.signal

  1. 头文件:#include <signal.h>

  2. 原型:

  3. man 2 signal:

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler); 
    
  4. man 3 signal:

    void (*signal(int sig, void (*func)(int)))(int); 
    
  5. 功能: 注册一个信号函数 一般在进程刚开始的时候

  6. 参数:

    1. signum : 信号号

    2. handle :信号的处理方式

      1.忽略:SIG_IGN

      2.默认:SIG_DFL

      3.捕捉:指向自定义函数的指针

    ​ 函数指针,指向一个返回值:void参数是int类型的函数指针

    ​ signal函数会将该signum信号量和函数指针相绑定

  7. 返回值:

    1. 成功:返回一个函数指针指向上一次所执行的函数,保留一下
    2. 失败:SIG_ERR

代码:

#include<stdio.h>
#include <signal.h>
#include <unistd.h>
void myfun(int signum)//自己定义的中断函数也叫中断事件
{
	printf("哈哈,关不掉吧!\n");

}

int main(int argc, const char *argv[])
{
	//注册信号函数
	//进行信号的捕捉,将SIGINT信号的处理方式改成自己的处理方式
	//去执行我自己的功能
	if(signal(SIGINT,myfun) == SIG_ERR)
	{
		perror("signal");
		return -1;
	}
	while(1)
	{
		printf("主线程在干自己的事件\n");
		sleep(1);
	}
    return 0;
}

作业:利用signal函数去完成最佳回收僵尸进程的方式

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void timeout(int sig)
{
    if(sig==SIGALRM)
            puts("Time out");
    alarm(2);
}
void keycontrol(int sig)
{
    if(sig==SIGINT)
            puts("CTRL + C pressed");
}
int main()
{
    int i;
    signal(SIGALRM,timeout);
    signal(SIGINT,keycontrol);
    alarm(2);
    for(i=0;i<3;i++)
    {
        puts("wait...");
        sleep(100);
    }
    return 0;
}