进程与线程的同步与互斥

时间:2022-06-11 23:29:07
进程间通信 




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() 刚才另一端的信号量,提示对方可卖票了。
如此循环直到所有的票都正常卖出。