1、早期的古老通信模式: 管道 信号 ====》os都支持
2、较新 IPC 对象: 消息队列 共享内存 信号量集 ===》system V POSIX
3、BSD 系列: socket ====》网络
管道:===》无名管道 有名管道
无名管道 ===》管道 ====》通信管道
1、只能用于具有亲缘关系的进程间使用。
2、半双工通信,有固定的读端和写端。
3、特殊的系统文件,可以支持文件IO。
4、管道的数据存储方式类似队列,先进先出。
5、管道的默认容量大小是64k字节 ===》ulimit -a ====>4k 早期的大小
6、管道默认的读操作会阻塞等待,如果写操作满了的时候也会阻塞等待。
7、管道的读端存在时候,写管道才有意义,否则程序会退出。
无名管道的使用:
1、管道的创建与打开
int pipe(int fd[2]) ;
功能:通过该函数可以创建一个无名管道,同时打开该管道。
参数: fd[2] 要操作的管道名称,有两个元素
fd[0] 管道的固定的读端
fd[1] 管道的固定的写端
返回值: 成功 0
失败 -1
注意:在创建新的进程之前就应该先创建管道,之后之间的资源可以
通过管道传递。
2、管道的读写
读: ssize_t read(int fd, void * buff,size_t count );
从fd中读count个字节的数据到buff中。
写: ssize_t write(int fd, const void * buff,size_t count)
从buff中取count个字节的数据写入到fd文件中。
3、管道的关闭
close(fd[0]);
close(fd[1]);
小练习:使用无名管道完成父子进程间发送信息,要求父进程动态获取用户的输入并写入到管道的写端。
子进程从管道的读端获取用户的输入信息,并打印输出到终端。
当输入“quit” 程序整体结束。
///////////////////////////////////////////////////////////////////////////////////////////////////
有名管道 ===》相同的管道特性
特点:1、可以用于不同进程间通信,不限于必须有亲缘关系。
管道的操作流程:
1、管道的创建 =====》mkfifo
int mkfifo(const char * filename,mode_t mode);
功能:通过该函数可以创建一个有名管道的设备文件出来。
参数:filename 要创建的管道名称+ 路径
mode 创建的管道的文件权限。
返回值: 成功 0
失败 -1;
2、管道的打开 =====》open
int open(const char * filename,int flag);
参数: filename要打开的有名管道文件名称
flag == 》O_RDONLY 只读
O_WRONLY 只写
O_RDWR 不能用。
3、管道的读写 ====>read write
读: ssize_t read(int fd, void * buff,size_t count );
从fd中读count个字节的数据到buff中。
写: ssize_t write(int fd, const void * buff,size_t count)
从buff中取count个字节的数据写入到fd文件中。
4、管道的关闭 ====>close
close(fd);
5、管道的卸载 ====>unlink
int unlink(const char * pathname);
功能:卸载或者删除已经创建的有名管道文件
参数: pathname 要卸载的管道文件名称+路径
返回值: 成功 0
失败 -1;
小练习: 编写程序从主函数传参的方式创建有名管道,并在该程序中获取用户输入的信息。写到有名管道中。
编写另一个程序,循环从有名管道中读取数据,并将数据实时打印到终端。
2、将fifo的创建和删除操作写成通用的工具,允许用户的指定创建和删除,比如
./fifo_tool -C fifo ====>会在当前目录下创建一个名称为fifo的有名管道文件
./fifo_tool -D fifo ===>会删除当前目录下的名称为fifo的管道文件。
//////////////////////////////////////////////////////////////////////////////
信号的发送
kill 函数 =====》支持的信号列表 kill -l ====>所有当前系统支持的默认信号
头文件: signal.h sys/types.h
函数: int kill(pid_t pid,int sig);
功能:给指定的pid进程发送sig信号。
参数:pid要接收信号的进程id
sig 要发送的信号编号 ====》kill -l 中的信号
kill -l 中前32 属于系统原始信号,也叫不稳定信号
后32个属于稳定信号。
返回值: 成功 0
失败 -1;
int raise(int sig);
功能:进程可以自己通过该函数给自己发送信号。
参数:sig 要发的信号的编号
返回值:成功 0
失败 -1;
闹钟函数和暂停函数
暂停函数 ====》pause() ====>while(1){sleep(1);}
int pause(void ) ====>执行该函数后程序暂停。
闹钟函数 =====》alarm() =====》定时时间到了发送 SIGALRM 信号给自己。
unsigned int alarm(unisgned int sec);======>指定间隔sec秒之后给自己发送信号。
默认的SIGALRM 信号会使程序终止运行。
信号的接收处理
三种方式: 默认处理 忽略处理 自定义处理
1、捕获信号和处理
void () (int);======>void fun(int arg);
void (*signal(int signum,void (*handler)(int)))(int);
signal(int signum , test);===>
test == void(*handler)(int);
typedef void (*SIGNAL)(int);
===> void (*signal(int signum,void (*handler)(int)))(int); == SIGNAL signal(int singnum,SIGNAL handler);
简化成: signal(int sig, void fun); ====>信号扑捉处理函数
参数: sig 要处理的信号
fun 信号的处理函数,如果其是:SIG_IGN 表示该程序对所有的信号做忽略处理
SIG_DFL 表示该程序对所有的信号做默认处理
fun 表示改程序有一个回调函数用来自定义处理
注意:在所有系统预制的信号列表中,9号SIGKILL 和 19 号的SIGSTOP信号不能被忽略处理。
信号在进程间通信中的缺陷:
1、不能发送大量数据,包括字符串。
2、发送的信号必须是双方约定好的。
3、发送的信号必须是系统预制的范围内的。
4、发送的信号在接收方必须有自定义处理,一般用SIGUSR1 SIGUSER2.其他信号有系统含义 建议不要使用。
key 的获取
1、私有key =====》IPC_PRIVATE === 0X00000000
2、测试key =====》ftok() ====>指定路径+字符
3、自定义key ====》0X12345678
key_t ftok(const char * pathname ,int pro_id);
功能:通过该函数可以以指定的路径为基本生成一个唯一键值。
参数:pathname 任意指定一个不可卸载的目录同时要求改目录不能被删除重建。
pro_id 一个数字,或者字符,表示将该值与参数1做运算之后的值作为键值。
返回值:成功 返回唯一键值
失败 -1;
作业:
使用信号通信方式完成指定程序对于用户的自定义信号处理,
如果程序收到SIGUSR1 信号就开始打印连续数据,如果收到SIGUSR2信号则打印停止。
其他所有信号尽量屏蔽。
./a.out ====>pid = 123
kill -10 123 ===>循环打印数据
kill -12 123 ====>打印停止
kill -1 123 ====>程序无响应
IPC 对象
1、查看对象信息 ====》ipcs -a
ipcs -q ====>只查看消息队列的对象信息
ipcs -m ====>只查看共享内存的对象信息
ipcs -s ====》只查看信号量的对象信息
基本操作流程:
key ====>申请或者创建IPC对象====》读写数据 ====》卸载对象
消息队列:
0、头文件:
sys/types.h
sys/ipc.h
sys/msg.h
1、申请消息队列;
int msgget(key_t key ,int flag);
功能:向内核提出申请一个消息队列对象。
参数: key 用户空间的键值
flag 消息对象的访问权限,但是如果是第一次
向内核提出申请,则需要添加IPC_CREAT 和 IPC_EXCL
返回值:成功 返回消息对象id
失败 -1
2、消息对象的操作:
发送消息:int msgsnd(int msgid,void * msgp,size_t size ,int flag);
参数:msgid 要发送到的消息对象id
msgp ====>要发送的消息结构体===》
struct msgbuf
{
long mtype; ////消息的类型
char mtext[N];////消息的正文,N 自定义的数据大小
};
size 要发送的消息正文的长度,单位是字节。
flag = 0 表示阻塞发送
= IPC_NOWAIT 非阻塞方式发送
返回值:成功 0
失败 -1;
接收消息:
int msgrcv(int msgid,void * msgp,size_t size,long type,int flag);
参数: msgid 要接收到的消息对象id
msgp ====>要接收的消息结构体变量,必须事先定义一个空变量,用来存储数据。
size ====》要接收的数据长度,一般是 sizeof(msgp);
type ====>要接收的消息类型
flag ====》接收消息的方式,0 表示阻塞接收 IPC_NOWAIT 非阻塞接收
返回值:成功 0
失败 -1;
练习:用两个程序完成如下功能:
a.out ===>用当前路径下的test目录创建消息队列的对象。并向该对象中发送获取到的用户输入信息
b.out ===>用同样的路径下的test目录来获取key值并从该消息对象中接收用户输入的消息。
IPC 对象的操作命令:
ipcs ===>查看命令 ===》ipcs -a ipcs -q ipcs -m ipcs -s ===》查看对象的当前信息
ipcs -l ipcs -lq ipcs -lm ipcs -ls ===>查看对象的默认上限值
ipcrm ===>删除命令 ===》ipcrm -q msgid ====>删除消息队列中队列id是msgid的对象
ipcrm -m shmid ====>共享内存对象删除
ipcrm -s semid ====>信号量集对象删除
3、消息队列对象的卸载:
int msgctl(int msgid,int cmd, struct msgid_ds * buff);
功能:调整消息队列的属性,很多时候用来删除消息队列。
参数: msgid 要操作的消息队列对象id
cmd ==>IPC_RMID ====>删除消息队列的宏
IPC_SET ====>设置属性
IPC_STAT ====》获取属性
buff ====》属性结构体
返回值:成功 0
失败 -1;
共享内存的操作流程:
0、头文件:
sys/shm.h
1、key值得创建
2、shmget 向内核申请共享内存对象
int shmget(key_t key ,int size ,int flag);
参数: key 用户空间的唯一键值
size 要申请的共享内存大小
flag 申请的共享内存访问权限,如果是第一次申请,则需要 IPC_CREAT IPC_EXCL;
返回值: 成功 shmid
失败 -1;
3、shmat 将内核申请成功的共享内存映射到本地
void * shmat(int shmid,const void * shmaddr,int flag);
参数: shmid 申请好的共享内存id
shmaddr ===》NULL 表示由系统自动查找合适的内存映射。
flag ====》对于挂载之后的内存的操作权限。0 表示可直接读写
SHM_RDONLY 表示只读权限
返回值:成功 返回映射后的可以使用的地址
失败 NULL;
4、读写共享内存 ====》类似操作堆区的方式
5、shmdt 断开本地与共享内存的映射
int shmdt(void * shmaddr);
参数: shmaddr 要断开的已经映射的地址,就是shmat的返回值。
返回值: 成功 0
失败 -1;
6、shmctl 删除共享内存操作对象
int shmctl(int shmid,int cmd ,struct shmid_ds *buff);
参数: shmid 要删除的共享内存对象
cmd 要操作的宏,IPC_RMID 表示删除对象
IPC_STAT 表示获取对象属性
IPC_SET 表示设置对象属性
buff 属性结构体对象
返回值:成功 0
失败-1
信号量集:
0、头文件
sys/sem.h
1、key 唯一键值
2、semget 向内核申请一个信号量集
int semget(key_t key ,int nsems, int flag);
参数:key 用户空间的唯一键值
nsems 要申请的信号量个数
flag 申请信号量的权限,如果是第一次申请则需IPC_CRATE IPC_EXCL;
返回值:成功 0
失败 -1
3、semop 操作信号量 ===》PV 操作 ====》信号量的加一 减一
int semop(int semid,struct sembuf * buff,size_t opts);
参数: semid 要操作的信号量集id
buff要操作的方式方法===》操作结构体
struct sembuf
{
short sem_num; //要操作的信号量的编号,一般都是从0 编号。
short sem_op; //要给该信号量进行的操作,1 V操作 释放资源
-1 P 操作 申请资源
0 阻塞等待
short sem_flg; //0 表示阻塞方式执行信号量
IPC_NOWAIT 表示非阻塞
SEM_UNDO 表示操作结束后返回原值
};
opts 要操作的信号量的个数。
返回值: 成功 0
失败 -1;
小练习:配合共享内存和信号量集两个方式完成A进程向B进程发送实时的消息,完成基本聊天功能。
要求:当输入quit的时候两个程序都退出
所有的信号量操作模块写成独立函数。分别是 my_sem_wait() my_sem_post();
4、semctl 删除信号量
int semctl(int semid, int semnum,int cmd ...);
功能:通过该函数可以调整信号量集中指定的信号量的属性。
参数:semid 要调整的信号量集的id
semnum 要调整的信号量编号
cmd 要执行的动作 IPC_RMID 表示要删除该对象
SETVAL 表示设置信号量的值
GETVAL 表示获取信号量的值
返回值:成功 0
失败 -1
gdb 调试core文件 ===》如果程序出现 seg... fault
1、先在编译文件的时候 加 -g 参数,使编译后的代码有调试符号。
2、设置系统允许生成core文件 ====》 ulimit -c unlimited
3、启动有问题的程序 执行一次 ===》./a.out ====>segmentaion fault (core dumped)====>程序出现严重错误并彻底退出
4、gdb a.out core ====>如果错误时在main函数中则能直接定位到错误的准确位置
5、如果处错误的位置不是在main函数中可以用 bt 命令,打印堆栈信息查看函数的调用过程。
作业:
用消息队列的方式完成进程1 给进程2 发送一个完整的文件。
//////////////////////////////////////////////////////////////////////////////////////////////////////
临界值的访问需要保证一致性 ===》互斥锁
操作流程:
1、定义互斥锁
在线程开始之前定义一个全局变量 ====》 pthread_mutex_t mutex;=====>其中mutex 就是互斥锁。
2、初始化互斥锁 ====》一般在定义多线程开始之前。
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t * attr);
功能:通过该函数可以将已经定义的互斥锁设置为默认属性的锁。
参数: mutex 定义好的互斥锁
attr 锁子的属性,NULL表示默认属性。
返回值:成功 0
失败 -1;
3、加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
4、解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
以上两个函数一般成对出现,其中mutex参数就是要加锁的锁子。
如果有先lock 则在unlock 之前的所有代码有可以保证属于原子操作
在执行被保护的代码过程中,其他需要用到该代码的程序会阻塞等待。
5、销毁锁
int pthread_mutex_destroy(pthread_mutex_t * mutex);
功能: 将已经不在使用的互斥锁销毁。
参数:mutex 要销毁的锁
返回值:成功 0
失败 -1;
问题:
1、该程序要表达什么样的用意
2、由几个线程在执行
3、_LOCK_的作用是什么
4、首次执行的结果是什么
5、要看到不同的输出结果要如何修改
线程同步 ===》多个线程 有顺序 配合完成一个任务。
线程同步的对象 ====》posix 无名信号量。====》PV操作
信号量的值是非负整数,主要是二值信号量 ===》0 1 ===》0 阻塞 1 通行。
公共头文件: semaphore.h
信号量的操作流程:
0、信号量的定义:
sem_t sem; ====>定义一个信号量。
1、信号量的初始化 =====》一般要放到线程创建之前。
int sem_init(sem_t *sem,int pshared, unsigned int value);
功能:通过该函数可以将指定的信号量做初始化赋值。
参数: sem 要初始化的信号量
pshared 0 线程间使用
非0 进程间使用
value 信号量的初始值,如果是0 表示默认开始就阻塞
1 表示默认处于允许通过。
返回值: 成功0
失败 -1;
2、信号量的P操作 ====》申请信号量判断是否可用
int sem_wait(sem_t *sem);
功能: 通过该函数可以判断信号量是否可以用,就是判断其当前值是否> 0,如果< 0则
程序会在该函数部分阻塞等待。如果>0 则该函数可以继续执行通过,并且sem= sem-1;
参数:要判断的信号量
返回值: 成功 0
失败 -1;
3、信号量的V操作 ===》释放信号量,表示可用。
int sem_post(sem_t * sem);
功能: 该函数可以正常执行通过,并且将指定的信号量sem= sem+1;
参数: sem 要操作的信号量
返回值: 成功 0
失败 -1;
4、辅助函数:
sem_trywait();===》sem_wait() 作用一样用于检测是否有可用信号量,但是不则阻塞。
sem_getvalue();====》获取当前线程的信号量的值。
int sem_getvalue(sem_t *sem ,int *value);
参数: sem 要获取的信号量地址
value 信号量的当前值
返回值:成功 0
失败 -1
可以做个小练习加深印象:
火车票售票系统,要求:用两个子线程分别模拟两个售票窗口,假设有100张车票,现在要求通过
两个窗口均匀的将车票卖出,考虑用信号量或者互斥锁来实现。
不能出现0 票和 小于0 的车票以及大于100 的车票卖出。
思路:应该有两个信号量组成同步操作。
首先要初始化两个信号量,一个要初始化成0 一个要初始化成1
第二开始执行过程中,线程1 和线程2 之间都先做 sem_wait();
第三步,当开始执行的线程执行完毕应该要 sem_post() 对方的信号量,提示他可以卖票了。
第四步,当对端完成卖票后要sem_post() 刚才另一端的信号量,提示对方可卖票了。
如此循环直到所有的票都正常卖出。