Linux线程间的同步和互斥
同步:有顺序的考虑
按照约定的顺序相互配合完成一件事情(红绿灯)
{释放 产生 资源+1(V操作);;申请 资源-1(p操作)}
信号量代表某一类资源;;
异步:你做你的我干我的
同步
1.先全局定义信号量sem_t sem_id;
2.在main函数中初始化信号量
3.p/v操作
1.线程间的通信方式------全局变量
2.线程间通信时,必须 使用同步和互斥机制
1、同步通信:多个任务,按照约定的顺序共同完成
2、posix中的同步机制:
无名信号量(基于内存的信号量)
有名信号量
3、信号量:
系统中资源的数量。
4、信号量操作方式,必须通过以下函数
sem_init
a)int sem_init(sem_t sem,int pshared,unsigned int value);
功能:
初始化信号量,指定信号量的使用范围和信号量的初始值。
头文件:
#include <semaphore.h>
参数:
sem:信号量首地址
pshared:信号量的使用范围(0:线程间 非0:进程间)
value:信号量的初始值
返回值:
成功:0
失败:-1;
sem_wait
b) int sem_wait(sem_t *sem);
功能:
p操作,申请资源 。(如果有资源,资源-1;若没有则阻塞等待)
参数:
sem:信号量首地址
返回值:
成功:0
失败:-1
sem_post
c)int sem_post(sem_t *sem);
功能:
V操作,释放资源,(如果有任务等待该资源,则唤醒任务,若没有任务,则资源+1)
参数:
sem:信号量首地址
返回值:
成功:0
失败:-1
/*===============================================
* 文件名称:pthread_test.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
在一个进程中创建两个线程,用线程1获取键盘上的数据,线程2打印键盘上的数据到终端,并且当线程1不输入时,线程2等待线程1输入
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
void *thread1(void *arg);
void *thread2(void *arg);
sem_t sem_id; //定义全局的信号量
char buf[32]={0};
//获取键盘输入的数据
void *thread1(void *arg)
{
while(1)
{
// scanf("%s",buf);
fgets(buf,sizeof(buf),stdin);
// V操作,释放资源(如果有等待该资源的线程将被唤醒;若没有则资源+1)
sem_post(&sem_id);
if(strncmp(buf,"quit",4)==0)
break;
}
pthread_exit(NULL);
}
//打印其数据
void *thread2(void *arg)
{
while(1)
{
//p操作,申请资源(如果有资源就资源数-1,若没有则等待)
sem_wait(&sem_id);
printf("buf=%s\n",buf);
if(strncmp(buf,"quit",4)==0)
break;
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
//创建线程之前,初始化信号量
//参数1:信号量首地址 参数2:指定信号量的使用范围(0:线程间)
if(sem_init(&sem_id,0,0)<0)
{
perror("init");
return -1;
}
pthread_t tid1,tid2;
if(pthread_create(&tid1,NULL,thread1,NULL)<0)
{
printf("create thread1 failed\n");
return -1;
}
pthread_detach(tid1);
if(pthread_create(&tid2,NULL,thread2,NULL)<0)
{
printf("create thread2 failed\n");
return -1;
}
pthread_detach(tid2);//非阻塞等待
while(1);
return 0;
}
hqyj@ubuntu:~/5.16$ gcc pthread_test.c -lpthread
hqyj@ubuntu:~/5.16$ ./a.out
asdaf
buf=asdaf
互斥
保护资源完整性
互斥机制:不能同时进行,当一个任务进行中,另一个任务阻塞等待
1、临界资源:
多个任务共同抢占的 全局资源。
2、由于多个任务抢占同一临界资源,会导致 资源数据不完整,会使用互斥机制来保护 临界资源的完整性。
3、临界区:
访问、操作临界资源的代码行20,21.....
4、锁粒度:
临界区越大,锁粒度越强;
5、同一个临界资源的所有临界区必须同时加锁和解锁 同一把互斥锁
在临界区之前加锁,临界区结束时必须解锁
6、死锁:
a)多个临界区时,加锁不解锁,就会形成死锁。
b)多个互斥锁A和B,任务1申请A锁,释放B锁,申请B锁,释放A锁。(交叉 加锁解锁)
7、如何避免死锁?
a)临界区加锁后必须解锁
b)尽量避免使用临界资源
c)同一临界资源只用一把锁
1.先全局定义互斥锁pthread_mutex_t mutex;
2.在main函数中初始化互斥锁
3.lock/unlock操作
pthread_mutex_init
a)int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t *attr)
功能:
初始化互斥锁
参数:
mutex:互斥锁
attr:互斥锁属性//NULL表示缺省属性
返回值:
成功:0;
失败:-1//返回错误码
pthread_mutex_lock
b)int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
申请互斥锁
参数:
mutex:互斥锁
返回值:
成功:0
失败:返回错误号
pthread_mutex_unlock
c)pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
释放互斥锁
参数:
mutex:互斥锁
返回值:
成功:0
失败:返回错误号
/*===============================================
* 文件名称:pthread_test1.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
1.创建两个线程thread1,thread2,一个线程赋值,另一个线程打印
2.因为当第一个线程value1=count时会消耗时间资源,此时第二个线程有可能因没有count赋值给value1和value2时,有可能value1==value2所以会打印出来。。
3.所以利用互斥锁
将线程1完成赋值完全之后,再让线程2作用
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg);
void *thread2(void *arg);
pthread_mutex_t mutex;
int value1,value2,count=0;
void *thread1(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);//申请互斥锁,加锁
count++;
value1=count;
value2=count;
pthread_mutex_unlock(&mutex);//释放互斥锁,解锁
}
}
void *thread2(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);//申请互斥锁,加锁
if(value1 != value2)
{
printf("value1=%d value2=%d count=%d\n",value1,value2,count);
}
pthread_mutex_unlock(&mutex);//释放互斥锁,解锁
}
}
int main(int argc, char *argv[])
{
if(pthread_mutex_init(&mutex,NULL)<0)
{
printf("mutex_init failed\n");
return -1;
}
pthread_t tid1,tid2;
if(pthread_create(&tid1,NULL,thread1,NULL)<0)
{
printf("thread1 create failed\n");
return -1;
}
if(pthread_create(&tid2,NULL,thread2,NULL)<0)
{
printf("thread2 create failed\n");
return -1;
}
while(1);
return 0;
}
未使用互斥锁
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
value1=304798371 value2=304798370 count=304798371
^C
使用互斥锁:
hqyj@ubuntu:~/5.16$ gcc pthread_test1.c -lpthread
hqyj@ubuntu:~/5.16$ ./a.out
^C
进程间传统通信方式
- 通信方式
传统进程间通信: 无名管道pipe、有名管道:fifo 信号:signal
system v IPC对象:共享内存(share memory),消息队列(message queue),信号灯集(semaphore)
BSD:套接字(socket)
无名管道pipe
- 无名管道 pipe
pipe()
int fd[2];
int pipe(int fd[2])
功能:
管道的创建
参数:包含两个元素的整型数组
返回值:
成功:0;
失败:-1;
在内核中开辟的管道空间,该管道通过文件描述符进行操作
数据单向传递->[-----]->
当管道建立时,得到fd[0],fd[1]
具备固定的读端fd[0]和写端fd[1],(只有父进程创建好的,子进程才能使用)
/*===============================================
* 文件名称:pthread_test2.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
进程间不能使用全局变量
创建两个子进程,其中子进程1输入数据,子进程2打印数据
如果不用pipe管道,则会出现,在子进程1中stdin向buf写入数据,子进程2无法获取已经存入数据的buf,即使buf为全局变量。因为在创建子进程的过程中会复制父进程main中的process1和process2,但不会开辟空间,只是复制名字。
所以我们需要单独在内核开辟一个空间,让子进程1和子进程2都可以访问这个空间,数据就可以共同调用。则我们需要在内核中创建一个无名管道去传输数据----读----写-----
子进程1,2在用户空间。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
//读数据
void process1();
//写数据
void process2();
int fd[2];
char buf[32]={0};
//读数据
void process1()
{
close(fd[0]);
//由于该进程用作写入数据,因此必须关闭读端
while(1)
{
fgets(buf,sizeof(buf),stdin);
//将数据写入管道的写端
write(fd[1],buf,strlen(buf));
}
}
//写数据
void process2()
{
//由于该进程用作读取数据,因此必须关闭写端
close(fd[1]);
while(1)
{
//将管道的数据读取到buf缓存
read(fd[0],buf,sizeof(buf));
printf("buf=%s\n",buf);
sleep(1);
}
}
int main(int argc, char *argv[])
{
//创建无名管道
if(pipe(fd)<0)
{
perror("pipe");
return -1;
}
printf("fd[0]=%d fd[1]=%d\n",fd[0],fd[1]);
pid_t pid=fork();
if(pid<0)
{
perror("fork1");
return -1;
}
else if(pid==0)
{
process1();
}
else
{
pid_t pid2=fork();
if(pid2<0)
{
perror("fork2");
return -1;
}
else if(pid2==0)
{
process2();
}
else
{
//父进程关闭读端和写端,因为内存中共有3个读端,而一个管道只能有一个读端
close(fd[0]);
close(fd[1]);
while(1);
}
}
return 0;
}
无名管道的特点:
1.只能在具有亲缘关系的进程间使用
2.具有固定的读端和写端,属于半双工通信
3.管道可以看做一个特殊的文件,无文件名只存在于内核当中
注意:
1.只有写端存在,读端才能读取数据,否则将会直接返回
2.写端存在,管道无数据,读端将会阻塞等待写入数据
3.当读端不存在,写端写入数据会出现,管道缓冲区满了,阻塞住
只有当缓冲区有空闲的字节,写端才能写入数据
4.只有管道的读端存在时,向管道中写入数据才有意义
否则,向管道中写入数据的进程将受到内核传来的SIGPIPE信号(通常Broken pipe错误),进程将被终止
/*===============================================
* 文件名称:pipe2_demo.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
子进程从键盘接收数据,父进程打印数据,利用进程间的通信(pipe无名管道)
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
void child_process();
int fd[2];
char buf[32]={0};
void child_process()
{
close(fd[0]);
while(1)
{
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
write(fd[1],buf,strlen(buf));
}
}
int main(int argc, char *argv[])
{
if(pipe(fd)<0)
{
perror("pipe");
return -1;
}
pid_t pid1=fork();
if(pid1<0)
{
perror("fork");
return -1;
}
else if(pid1==0)
{
child_process();
}
else
{
close(fd[1]);
while(1)
{
memset(buf,0,sizeof(buf));
read(fd[0],buf,sizeof(buf));
printf("buf=%s\n",buf);
}
}
return 0;
}
/*===============================================
* 文件名称:pipe3_demo.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
计算无名管道能存放多少数据
当读端开放时:写数据内存大小会达65535
当读端关闭时:写端会阻塞等待
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd[2]={0};
if(pipe(fd)<0)
{
perror("pipe");
return -1;
}
// close(fd[0]);//关读端写入数据
int count=0;
char buf[1024]={0};
while(1)
{
int ret=write(fd[1],buf,sizeof(buf));
count += ret;
printf("count=%d\n",count);
}
return 0;
}
。
。
。
count=47104
count=48128
count=49152
count=50176
count=51200
count=52224
count=53248
count=54272
count=55296
count=56320
count=57344
count=58368
count=59392
count=60416
count=61440
count=62464
count=63488
count=64512
count=65536
有名管道fifo
- 有名管道
mkfifo()
int mkfifo(const char *filename,mode_t mode);
功能:
创建管道文件
参数:
filename:要创建的管道
mode:指定管道的访问权限 0640
返回值:
成功:0;
失败:-1;一般没有返回值
1、有名管道特点:
1、既可以在亲缘关系的进程间通信,也可以在无亲缘关系的进程间进行通信。
2、有名管道可以通过路径名来指出,并且在文件系统中可见
3、进程通过文件I0来操作有名管道,但是不能使用lseek
4、有名管道遵循先进先出规则
2、注意:
1、管道中没有数据,读进程的read会阻塞。.
2、关闭写进程,读进程的read会立即返回。
3、有名管道的数据传递都是在内存中进行的。原因:open函数将磁盘搬到内核中去,所以磁盘中的fifo为0字节
/*===============================================
* 文件名称:fifo_write.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
通过有名管道fifo使得非亲缘关系的进程之间实现通信
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
//在指定的路径中创建有名管道文件
if(mkfifo("fifo.txt",0640)<0)
{
perror("mkfifo");
}
//打开有名管道文件,得到管道文件描述符
int fd = open("fifo.txt",O_WRONLY);
if(fd<0)
{
perror("open");
return -1;
}
while(1)
{
char buf[32]={0};
fgets(buf,sizeof(buf),stdin);
//向管道文件中写入数据
write(fd,buf,strlen(buf));
if(strncmp(buf,"quit",4)==0)
break;
}
close(fd);
return 0;
}
/*===============================================
* 文件名称:fifo_read.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if(mkfifo("fifo.txt",0640)<0)
{
perror("mkfifo");
}
int fd = open("fifo.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return -1;
}
while(1)
{
char buf[32]={0};
read(fd,buf,sizeof(buf));
printf("buf=%s\n",buf);
if(strncmp(buf,"quit",4)==0)
break;
}
close(fd);
return 0;
}
hqyj@ubuntu:~/5.16$ gcc fifo_write.c
hqyj@ubuntu:~/5.16$ ./a.out
mkfifo: File exists
abcdefg
hqyj@ubuntu:~/5.16$ gcc fifo_read.c
hqyj@ubuntu:~/5.16$ ./a.out
mkfifo: File exists
buf=abcdefg
要分开编译,因为有两个主函数main,不能在一起gcc编译
![
](C:\Users\孤独memories\AppData\Roaming\Typora\typora-user-images\image-20230516191717480.png)
信号通信
- 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2. 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用
它来通知用户空间进程发生了哪些系统事件。
3. 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢
复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟
直到其阻塞被取消时才被传递给进程
为什么使用信号通信:
1.后台进程需要使用信号
2.非亲缘关系进程,不能使用pipe
3.非文件IO操作进程,不能使用fifo
则我们选择信号通信---标准IO通信
信号的生存周期: 内核进程、用户进程---------------不包括信号处理
用户进程对信号的响应方式:
忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
执行缺省操作: Linux对每种信号都规定了默认操作
kill()
int kill(pid_t pid, int sig);
头文件:
include <signal.h>
inlcude <sys/types.h>
功能:
发送信号给某一进程
参数:
pid:进程pid号
sig:指令信号
返回值:
成功:0;
失败:-1;
raise()
int raise(int sig);
头文件:
include <signal.h>
inlcude <sys/types.h>
功能:
发送信号给当前进程
参数:
sig:指令信号
返回值:
成功:0;
失败:-1;
hqyj@ubuntu:~/5.16$ kill -l//指令信号
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
/*===============================================
* 文件名称:signal_kill.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("hello world\n");
sleep(1);
//kill向某个pid号对应的进程发送信号
// kill(getpid(),SIGKILL);
kill(getpid(),4);
while(1)
{
printf("--------\n");
sleep(1);
}
return 0;
}
hqyj@ubuntu:~/5.16$ gcc signal_kill.c
hqyj@ubuntu:~/5.16$ ./a.out
hello world
非法指令 (核心已转储)
alarm()和pause()
alarm()和pause()
alarm()也称为闹钟函数,它可以在进程中设置一个定时器。当定时器指定的时间到时,内核就向进程发送SIGALARM信号。
pause()函数是用于将调用进程挂起直到收到信号为止。
unsigned int alarm (unsigned int second);
int pause(void);
头文件:
#include <unistd.h>
功能:
发送信号给当前进程
参数:
second:指定秒数
返回值:
成功:0;
失败:-1
/*===============================================
* 文件名称:signal_alarm.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = alarm(3);//设置一个闹钟,返回0
printf("ret=%d\n",ret);
// sleep(1);
ret = alarm(3);//设置一个闹钟,返回第一个闹钟的剩余时间
printf("ret=%d\n",ret);
pause();//将当前进程挂起,直到收到内核的SIGALRM信号,终止进程,不会去打印下面的。
printf("----------\n");
return 0;
}
hqyj@ubuntu:~/5.16$ gcc signal_alarm.c
hqyj@ubuntu:~/5.16$ ./a.out
ret=0
ret=3
闹钟
signal()
使用signal函数处理时,需要指定要处理的信号和处理函数
void (*signal (int signum, void ( *handler)(int)))(int);
头文件:
#include <signal.h>
功能:
设定信号signum的对应方式为handler函数
参数:
signum:指定信号
handler:SIG_IGN:忽略该信号
自定义的信号处理函数指针
返回值:
成功:设置之前的信号处理方式;
失败:-1
/*===============================================
* 文件名称:signal_ignore.c
* 创 建 者:memories
* 创建日期:2023年05月16日
* 描 述:
================================================*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int no)//形参no表示调用该函数时,传递的信号数值;也就表示当前是哪一个信号被触发
{
switch(no)
{
case 2: printf("welcome to chengdu\n");
break;
case SIGTSTP:printf("stop\n");
break;
}
}
int main(int argc, char *argv[])
{
// signal(SIGINT,SIG_IGN); //注册信号,设置信号SIGINT的处理方式为SIG_IGN忽略方式
signal(SIGINT,handler);//设置信号SIGINT处理方式为handler方式
signal(SIGTSTP,handler);
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
hqyj@ubuntu:~/5.16$ gcc signal_ignore.c
hqyj@ubuntu:~/5.16$ ./a.out
hello world
hello world
hello world
hello world
hello world
^Cwelcome to chengdu
hello world
hello world
^Zstop
hello world
hello world
hello world
hello world
hello world
^\退出 (核心已转储)
ctrl \ 结束
将信号名SIGINT-->ctrl C 转变为handler函数的处理方式
将信号名SIGTSTP-->ctrl Z 转变为handler函数的处理方式