Linux操作系统:进程与线程
Linux进程
进程和程序的区别
程序:指的是编译好的可执行文件,存放在磁盘上指令和数据的有序集合,程序是静态的,没有任何执行的概念,例如编译源文件之后的a.out文件、exe文件等。
进程:一个独立可调度的任务,执行一个程序所分配资源的总称,进程是程序的一次执行过程,进程是动态的,包括创建、调度、执行和消亡。
程序包含正文段、用户数据段,而进程除了包含这两个,还包括系统数据段,例如程序计数器(或称指令计数器,PC)存储下一条指令的地址、CPU的所有寄存器值(用于控制和计算等)、存储临时数据的进程堆栈等。
进程的结构
进程的标识:进程号(Process Identity Number,PID),例如我们在Linux使用不挂起nohup操作,会返回一个进程号,能够唯一地标识一个进程让我们找到。
操作系统记录了进程的PID,还会标识进程所属用户的UID,记录该进程分配的资源(内存、IO设备、文件)、进程的运行情况(CPU、磁盘、网络使用)等,这些都被记录在进程控制块(Process Control Block,PCB)中。
Linux进程包含三个段(这是给进程自身使用的):
数据段:存放的是全局变量,常数、动态数据分配的数据空间(malloc申请的空间)等。
代码段:存放的是程序中的代码指令。
堆栈段:存放的是函数返回的地址、函数的参数、程序中的局部变量。
进程在运行时,CPU和操作系统会为进程开辟一块虚拟空间,如32位系统是
程序执行:程序运行时首先会把数据和代码读入内存,操作系统会为进程创建PCB来管理控制,因此PCB是面向操作系统的,待执行完毕操作系统会回收PCB。PCB是进程存在的唯一标志。
Linux的几类进程:
交互进程:该类进程由shell控制和运行,交互进程既可以在前台,也可以在后台运行(失去交互,只能打印)
批处理进程:不属于某个终端,也不需要和用户交互,可以进行大批量的数据处理。
守护进程:在后台运行的进程,在Linux启动时开始执行,系统关闭时结束。
进程的运行状态
运行态R:进程正在运行或者准备运行。
等待态:进程在等待一个事件发生或者某个系统资源。可中断为D,不可中断为S。
停止态(T,或称暂停态):进程被中止,例如Linux使用ctrl+z可以暂时停止一个进程。
僵尸态(Z):这是一个已经中止的进程,但是进程的资源还没有被回收。
死亡态(X):进程终止,资源被回收,在ps中不可见。
进程切换关系:
时间片:时间片是多任务处理的一项技术,指的是运行当前进程的固定时长。CPU是串行运行的结构,当当前进程中的时间片被耗尽,CPU会调度其他进程,消耗其他进程的时间片,时间片一般是几毫秒,在用户的视角看去就像是CPU在同时并行地执行这些任务一样,这种机制较好地共享了CPU资源。
Linux进程相关指令
ps:查看进程。
top:动态显示系统中的进程
nice:按用户指定的优先级运行进程
renice:改变正在运行进程的优先级
kill:杀死进程(发生信号)
bg:将挂起的进程后台运行(ctrl+z,返回中止的编号,bg
编号 即可切换到后台)
fg:将后台运行的进程放在前台运行
进程的调用函数
1. 进程的创建
1 | /**************************************** |
fork是用于创建新的的进程,使用fork会产生两个进程,且通过父进程来产生子进程的。以下代码使用测试,首先bash终端创建了父进程,在父进程中返回的是子进程的pid(大于0);子进程创建后返回的是0;注意打印时要加上换行符,以免字符在缓冲区中未输出就进入了循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int main(){
pid_t pid;
pid=fork();
if(pid<0){ //小于0创建失败
perror("fork failed!\n");
}
else if(pid==0){ //等于0的是子进程
printf("child process!\n");
printf("parent:%d\tchild:%d\n",getppid(),getpid());//获取打印子进程ppid和pid
while(1);
}
else{
printf("parent process!\n");
printf("parent:%d\tchild:%d\n",getppid(),getpid());//获取打印父进程ppid和pid
while(1);
}
return 0;
}子进程几乎拷贝了父进程的所有内容,包括代码、数据、系统数据段的PC值、栈数据、父进程打开的文件等。
父子进程有独立的地址空间,互不影响;当在进程中修改全局变量、静态变量都互不影响。
父进程先结束,子进程会被init(pid=1)进程收养,子进程变成后台进程;子进程先结束,父进程如果没有及时回收,子进程会变成僵尸进程在进程向量中(要避免这种情况)。
2. 进程退出
1 | /**************************************** |
其中status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般0表示正常结束,其他数值表示非正常结束(或者我们去赋予意义)。实际中可以使用wait系统调用接收子进程的返回值,进行相应的处理。
_exit和exit函数的区别:
_exit()函数的作用:直接中止进程,清除其使用的内存空间,消耗其在内核中使用的各种数据结构。
exit()函数的作用:在_exit()的基础上做了一些包装,执行退出前加了若干道工序,例如会调用由atexit注册的函数,刷新使用的stdio缓冲区,将缓存数据写入文件,例如使用printf打印内容没有调用fflush或者添加换行符,exit()会把对应的内容输出到文件或者终端,最后关闭stdio流和删除临时文件。
总而言之, exit适合于普通程序的结束,_exit适用于那种需要迅速中止,不需要执行标准清理流程的情况(例如多进程程序中使用_exit可以避免对共享stdio流数据造成破坏)。
1 | int main(){ |
3. 进程阻塞
1 | /**************************************** |
如果子进程比父进程先退出,会产生僵尸进程Z,为了避免这种情况,常常在父进程中使用函数wait代表父进程阻塞,等待子进程退出回收资源。此外子进程可以通过参数指定退出的状态,父进程使用wait进行接收。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21int main(){
pid_t pid;
pid = fork();
if(pid<0){
perror("fork failed!\n");
}
else if(pid==0){
printf("child process!\n");
exit(3); //以异常状态3退出,给wait处理
}
else{
//wait(NULL); //忽略子进程退出状态,父进程阻塞,等待子进程退出回收资源
int wstatus;
wait(&wstatus);
printf("wstatus:%d\n",wstatus); //768
printf("wstatus:%d\n",WEXITSTATUS(wstatus)); //宏转换后为3
printf("parent process!\n");
while(1);
}
return 0;
}
除了wait函数,常用的还有waitpid函数,实现了比wait更加具体、灵活的阻塞功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/****************************************
头文件:
#include <sys/types.h>
#include <sys/wait.h>
函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);
函数参数:
pid>0:代表传入子进程号,只等待pid号的子进程,不管其它子进程,pid号未退出会一直等待。
pid=-1:等待任何一个子进程退出,此时作用同wait
pid=0:等待其组ID等于调用进程的组ID的任一子进程
pid<-1:等待其组ID等于pid的绝对值的任一子进程
wstatus:同wait函数
options:
若为WNOHANG,不阻塞,若此时pid的子进程不是立即可用,则waitpid不阻塞,返回0;
若为0,同wait,父进程阻塞等待子进程退出。
函数的返回值:
正常:结束子进程的进程号
使用选项WNOHANG且没有子进程结束时:返回0
调用出错:-1
****************************************/
WNOHANG类似一种轮询机制,常常被设计于周期性地询问子进程是否结束以回收资源,而无需一直等待无法执行其他代码,提高了灵活性。以下代码实现了一个简单功能:子进程5秒延时退出,父进程每隔1秒询问子进程,最后打印子进程退出状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int main(){
pid_t pid;
pid = fork();
if(pid<0){
perror("fork failed!\n");
}
else if(pid==0){
printf("child process!\n");
sleep(5);
exit(3);
}
else{
int wstatus;
printf("parent process!\n");
while(waitpid(pid,&wstatus,WNOHANG)==0){
sleep(1);
}
printf("wstatus:%d\n",WEXITSTATUS(wstatus));
printf("child end!\n");
}
return 0;
}
exec函数族
exec函数族提供了一种在进程中启动另一种程序执行的方法,exec不像fork,它不会创建新的进程,它可以根据文件名、目录等找到可执行文件(可以是二进制文件bin,也可以是Linux下可执行的脚本),并且用它来取代原来进程的数据段、代码段、堆栈段,新程序执行从main函数开始,执行完成后进程内容除了进程号外,其他内容都被替换掉了。
1 | /**************************************** |
实际上exec函数族实现的功能是类似的,只是用法上有一点区别,也体现在函数的命名上:
含l的:说明参数可以使用列表表示,以NULL结尾,如例1
含v的:参数使用数组表示,要定义指针数组,如例3
含p的:会自动在当前环境变量搜索可执行文件,无需指定可执行文件路径,如例2、4
含e的:可以指定自己的环境变量,自动搜索可执行文件路径。如例5
注意:在单个进程中exec函数只有单个使用是有效的,叠加不会获得重复的效果。
应用的例子和语法: 1
2
3
4
5
6
7
8
9
10
11execl("/bin/ls","ls","-l",NULL); //例1
execlp("ls","ls","-l",NULL); //例2
char *arr[]={"ls","-l",NULL};
execv("/bin/ls",arr); //例3
execvp("ls",arr);//例4:p无需指定可执行文件路径
//环境变量
char *envp[]={"PATH=Test",NULL}; //定义新的环境变量
execle("myls","myls",NULL,envp);//例5:myls没有实现,所以没有输出
Linux守护进程
Linux守护进程是一种在后台运行的特殊程序,设计用于执行某些任务、系统响应、用户操作等,通常在系统启动时启动,持续运行至系统关闭,很少或者基本不与用户交互,也不依赖于终端,能够在没有用户登录时运行,守护进程一些常见的例子包括网络服务的守护进程(HTTP服务器、邮件传输代理),系统服务的守护进程(日志、定时服务)、数据库服务(MySQL的mysqld)等。
创建一个守护进程常见的步骤是:
创建子进程并且中止父进程,子进程会被init(pid=1)收养,成为后台进程不再依赖于终端。
setsid函数改变会话ID,成为会话组组长,确保进程不会再获得控制终端。
chdir函数改变当前的工作目录(一般是移到根目录),确保守护进程不会占用其他挂载的文件系统。
umask函数设置文件权限掩码,允许创建任何权限的文件。
关闭从父进程继承的所有打开过的文件。
1 | int main(){ |
运行后使用ps ux
观察进程,?表示为守护进程,小s代表此进程是会话组组长,初始化是成功的。
Linux线程
有了进程后,CPU从只能串行地执行任务,变成了能够“并行地”执行任务。例如可以在操作系统上同时运行多个应用,但是这种并发对实际的需求仍然是不够的,同一个应用的需求也是多样的,例如同时进行文字、视频聊天、浏览文章等。因此引入了线程的概念进一步提高任务的并发性。
线程是一个CPU基本的执行单元,也是程序执行流的最小单位,一个进程至少有一个主线程,可以有多个子线程,线程之间可以并发同时处理各种各样的任务。引入线程后,进程只作为系统资源分配(除了CPU)的基本单元,如内存地址、文件描述符等。传统的进程间并发因为环境不同,系统开销很大,而线程并发保持在同一个进程空间中,系统开销比较小,线程间的通信也容易实现。
1. 多线程编程
1. 线程的创建
1 | /**************************************** |
测试函数:创建子线程打印简单提示,但注意,打印的结果取决于CPU的调度策略,有可能CPU执行了子线程和主线程,打印了Main和Pthread两个信息,也有可能执行完主线程打印main后直接结束进程了(即不保证子线程一定执行)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *printTips(void *arg){
printf("Pthread Working!\n");
return NULL;
}
int main(){
pthread_t tid;
if(pthread_create(&tid,NULL,printTips,NULL)!=0){
perror("pthread create failed!\n");
return -1;
}
printf("Main Working!\n");
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *printTips(void *arg){
int temp=*((int *)arg); //将void*参数强转int*,再解引用打印
printf("Pthread Working,Value=%d\n",temp);
return NULL;
}
int main(){
pthread_t tid;
int a=100;
if(pthread_create(&tid,NULL,printTips,&a)!=0){
perror("pthread create failed!\n");
return -1;
}
sleep(1);
printf("Main Working!\n");
return 0;
}
2. 线程的阻塞
上面的代码中主进程可能未及执行子线程就已经退出了,许多情况下需要避免这种情况;多线程编译就提供了这样一个函数来处理,pthread_join
可以让主线程暂时阻塞,等待某个子线程执行完了才退出或者执行主线程后续的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_join(pthread_t thread, void **retval);
函数参数:
thread:要等待的线程
retval:指针*retval指向线程返回的参数(也即线程退出返回的状态值,不需要接收置为NULL)
返回值:
成功:返回0
失败:返回错误号
****************************************/1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *printTips(void *arg){
int temp=*((int *)arg);
printf("Pthread Working,Value=%d\n",temp);
return NULL;
}
int main(){
pthread_t tid;
int a=100;
if(pthread_create(&tid,NULL,printTips,&a)!=0){
perror("pthread create failed!\n");
return -1;
}
pthread_join(tid,NULL);
printf("Main Working!\n");
return 0;
}pthread_join
还有一个重要的功能就是能够回收子线程的资源。线程的状态有可结合(joinable)和分离(detacted)两种,使用pthread_create
创建的线程默认是非分离的,也就是子线程结束后资源不会马上自动回收,而是保留其线程ID、退出状态直到其他线程join它或者进程结束,这种机制使得子线程能够再被复用,但这在一定情况下会导致内存泄漏。
避免这种问题第一种解决方法就是使用pthread_join
阻塞等待并且回收资源,第二种就是使用pthread_detach
来将线程设置成分离状态,这样子线程结束的时候就会立刻自动回收资源。
1
2
3
4
5
6
7
8
9
10
11
12头文件:
#include <pthread.h>
函数原型:
int pthread_detach(pthread_t thread);
函数参数:
thread:需要分离的线程号
返回值:
成功:返回0
失败:返回错误号1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void *printTips(void *arg){
int temp=*((int *)arg); //将void*参数强转int*,再解引用打印
printf("Pthread Working,Value=%d\n",temp);
return NULL;
}
int main(){
pthread_t tid;
int a=100;
if(pthread_create(&tid,NULL,printTips,&a)!=0){
perror("pthread create failed!\n");
return -1;
}
sleep(1);
//使用pthread_detach进行分离转换,失败打印错误
if (pthread_detach(tid) != 0) {
perror("Failed to detach thread");
}
printf("Main Working!\n");
return 0;
}
3. 线程的退出
对于分离线程而言,pthread_exit
是标准退出方法,能够代替退出子线程,而且会自动回收资源,作用和return是等效的。而对于更多使用的非分离线程而言,pthread_exit
能够回收子线程的部分资源,而保留其线程ID和退出状态直到其他线程join掉它们,此外pthread_exit
能够将堆区变量、静态区等传递给其他线程,在嵌套情况下pthread_exit比return更加灵活。其他线程使用pthread_join接收线程的状态,如果是来自动态分配的空间,需要进行手动释放,此时这个堆空间的销毁任务就落到了接收值的进程上。
1
2
3
4
5
6
7
8
9
10
11/****************************************
头文件:
#include <pthread.h>
函数原型:
void pthread_exit(void *retval);
函数参数:
retval:线程返回的参数(也即线程退出返回的状态值,不需要接收置为NULL)
****************************************/
1 |
|
4. 线程的取消
1 | /**************************************** |
5. 标志位+双线程实现循环输入循环输出
代码实现了使用双线程循环接收和打印输出,主线程负责使用fgets进行字符接收,子线程负责每接收一段字符就打印,当字符长度大于32(buf的大小)时,会实现分段输出打印,直到输入quit,进程结束。
有若干细节问题需要讨论:
1.
首要方案是通过主线程接收参数,创立线程传入参数在子线程打印,但是反复接收参数创立子线程会造成多个子线程创建和销毁,增大了开销,因此选择的方案是使用全局变量buf,使子线程、主线程都能够访问。
flag的作用:实现了主线程和子线程的调度,当接收完置为1,flag=1时会进入子线程打印后马上被置为0,置为0才可以重新进入主线程接收输入。也即flag第一个作用是允许其在子线程执行一次打印,防止在子线程无限循环。第二个作用是在主线程中,只接收一次字符,因为在主线程时间片中fgets可以反复的接收,最后输入超过32字符的字符串,得到的buf只是最后输入的32个字符,前面的字符会被覆盖。
fgets和scanf:fgets的好处在于当输入超过指定长度的字符串,会首先读入31个字符(\0结尾)剩余的字符串会停留在输入缓冲区,在下一次的fgets中被捕获和输出。而scanf有可能因为缓冲区越界,不能正确自动捕获剩余的字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
char buf[32]; //用于存放输入内容
int flag=0; //标识主线程和子线程
void *printValue(void *arg){
printf("Pthread Working!\n");
do{
if(strncmp(buf,"quit",4)==0) //输入为quit,退出
break;
else if(flag==1){ //主线程输入
printf("buf:%s\n",buf); //打印
flag=0; //标志改变
}
}while(1);
pthread_exit(NULL);
}
int main(){
//创建线程
pthread_t tid;
if(pthread_create(&tid,NULL,printValue,NULL)!=0){
perror("pthread create failed!\n");
return -1;
}
do{
if(flag==0){ //flag==0,打印完才接收输入,否则会在时间片内反复接收
fgets(buf,32,stdin);
if(strncmp(buf,"quit",4)==0){
break;
}
flag=1; //标志位改变
}
}while(1);
pthread_join(tid,NULL);
return 0;
}
2. 线程同步和互斥机制
1. 线程间同步
同步(Synchronization)指的是多个线程(任务)按照约定的顺序完成一件事情。例如上述例子我们使用了flag来标识两个线程的顺序。Linux中线程同步机制是依靠信号量进行的,信号量来决定线程是继续运行还是阻塞等待。
三个重要信号量函数(PV操作)
信号量三个重要函数包含了信号量初始化、信号量释放资源、信号量消耗资源三个部分,初始时设置资源量适应我们代码功能需求,通过信号量来控制资源数量,当资源数量为0时,线程进入阻塞等待,直到另一个线程释放了资源使得资源数大于0时,线程继续执行,以下是三个函数介绍:
信号量初始化:sem_init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/****************************************
头文件:
#include<semaphore.h>
函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
函数参数:
sem:指向信号量对象的指针
pshared:为0表示信号量仅在创建该信号量的进程的线程之间共享,如果非0表示该信号量在各进程之间共享。
value:信号量的初始值,根据程序需要自定义设计,表示可以的资源数量。
返回值:
成功:0
失败:-1
****************************************/sem_wait
1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include<semaphore.h>
函数原型:
int sem_wait(sem_t *sem); //等待sem资源数大于0就开始执行线程,且自动消耗一次资源
函数参数:
sem:指向信号量对象的指针
返回值:
成功:0
失败:-1
****************************************/sem_post
1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include<semaphore.h>
函数原型:
int sem_post(sem_t *sem); //释放一次sem资源,别的线程可能会因此被激活
函数参数:
sem:指向信号量对象的指针
返回值:
成功:0
失败:-1
****************************************/
信号量+双线程实现循环输入和输出
通过信号量而不是标志位,实现了和上面标志位+双线程功能一致循环输入输出功能。sem1初始化为资源数0,每输入一次post一次,激活子线程的wait并且打印,实现了循环输入和输出;但是此时没有解决单时间片内多次输入、多次post的问题,因此增加信号量sem2用于控制是否接收输入,初始化资源数为1(确保第一次能够进入读取输入模块),读取输入到buf消耗sem2的资源数,子线程打印完才重新增加资源数,确保了单时间片内只会执行1次读取输入操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
sem_t sem1; //用于调度主线程、子线程执行顺序,保证输入一个输出一个
sem_t sem2;//处理超过32字符的读取,如果超过此信号量能防止读取2次、打印两次
char buf[32]; //用于存放输入内容
void *printValue(void *arg){
printf("Pthread Working!\n");
do{
if(strncmp(buf,"quit",4)==0) //输入为quit,退出
break;
sem_wait(&sem1);//等待sem1有资源才开始执行,且等到了资源数就-1;
printf("buf:%s\n",buf); //打印
sem_post(&sem2);
}while(1);
pthread_exit(NULL);
}
int main(){
//创建线程
pthread_t tid;
sem_init(&sem1,0,0);
sem_init(&sem2,0,1);//初始化资源为1,确保刚开始能进去;
if(pthread_create(&tid,NULL,printValue,NULL)!=0){
perror("pthread create failed!\n");
return -1;
}
do{ sem_wait(&sem2); //子线程打印完了才有资源给主线程
fgets(buf,32,stdin);
sem_post(&sem1); //获取到数据,申请资源数1,让子线程能够进行
if(strncmp(buf,"quit",4)==0){
break;
}
}while(1);
pthread_join(tid,NULL);
return 0;
}
2. 线程间互斥
互斥锁(Mutual exclusion,Mutex)是一种编程构造,用于防止多个线程同时执行某个代码段,这个代码段通常被称为临界区(Critical Section)。临界区指的是共享资源的(共享文件、内存、设备)的代码区域,为了确保数据的完整性和一致性,这些资源只能同时被单个线程安全地使用。线程必须先获取互斥锁才能访问临界资源,访问完资源后再释放该锁。当某个线程获取锁后,其他线程申请锁会一直阻塞等待锁的释放。
互斥锁函数
man
命令没有互斥锁函数条目,需要先安装:
sudo apt-get install manpages-posix-dev
申请互斥锁: 1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁指针
返回值:
成功:0
失败:错误号
****************************************/
释放互斥锁: 1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁指针
返回值:
成功:0
失败:错误号
****************************************/
互斥锁简单应用
假设现在有一个共享内存数组,主线程负责将数组元素逐一颠倒,子线程负责输出,那么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int shareNum[]={1,2,3,4,5,6,7,8};
//子线程函数负责打印
void *printValue(void* arg){
do{
for(int i=0;i<8;i++){
printf("%d",shareNum[i]);
}
puts(""); //注意换行符是必须的,否则会堵在输入缓冲器没有输出
sleep(1);//一秒打印一次
}while(1);
pthread_exit(NULL);
}
int main(){
pthread_t tid;
if(pthread_create(&tid,NULL,printValue,NULL)!=0){
perror("Pthread create failed!\n");
return -1;
}
//主线程负责交换
do{
for(int i=0;i<4;i++){
int temp=shareNum[i];
shareNum[i]=shareNum[7-i];
shareNum[7-i]=temp;
}
}while(1);
pthread_join(tid,NULL);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int shareNum[]={1,2,3,4,5,6,7,8};
pthread_mutex_t mlock;//全局变量声明互斥锁指针
void *printValue(void* arg){
do{
pthread_mutex_lock(&mlock);//子线程申请锁
for(int i=0;i<8;i++){
printf("%d",shareNum[i]);
}
puts(""); //注意换行符是必须的,否则会堵在输入缓冲器没有输出
//sleep(1);//一秒打印一次
pthread_mutex_unlock(&mlock);//子线程释放锁
}while(1);
pthread_exit(NULL);
}
int main(){
pthread_t tid;
if(pthread_create(&tid,NULL,printValue,NULL)!=0){
perror("Pthread create failed!\n");
return -1;
}
do{
pthread_mutex_lock(&mlock); //主线程申请锁
for(int i=0;i<4;i++){
int temp=shareNum[i];
shareNum[i]=shareNum[7-i];
shareNum[7-i]=temp;
}
pthread_mutex_unlock(&mlock);//主线程释放锁
}while(1);
pthread_join(tid,NULL);
return 0;
}
主线程、子线程的互斥锁顺序是随机的,这有时候也不符合我们的预期,要解决这个问题,就要使用到线程条件变量。
线程条件变量
线程条件变量常与互斥锁配合使用,旨在互斥锁中实现一个同步机制,能够自定义调度互斥锁线程的调用顺序。
初始化条件变量: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数:
cond:条件变量
attr:一般置为NULL,表示使用默认参数初始化条件变量。
restrict是一个C99标准引入限定符,旨在告诉编译器该指针是唯一访问数据对象的方式,该地址不会被共享给其他线程,从而允许编译器进行额外优化。
返回值:
成功:0
失败:返回错误号
****************************************/
无条件等待条件变量信号:pthread_cond_wait
一般会用于lock之后,wait隐含了unlock操作,阻塞等待时会解开互斥锁给其他线程使用,其他线程向该线程的条件变量发送信号后当前进程退出阻塞,lock获取互斥锁执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数:
cond:条件变量
mutex:互斥锁指针
返回值:
成功:0
失败:返回错误号
****************************************/
给条件变量发送信号: 1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
参数:
cond:条件变量
返回值:
成功:0
失败:返回错误号
****************************************/
销毁条件变量: 1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include <pthread.h>
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond:条件变量
返回值:
成功:0
失败:返回错误号
****************************************/
线程条件变量+互斥锁实现同步调度
使用条件变量使得能够有序调度主线程和子线程的执行,其逻辑是如果子线程获取锁,而主线程没有执行完毕,子线程会先交出锁;但这里有一个瑕疵,如果是主线程先获取到锁,那么主线程会执行颠倒,而不会保证进入子线程中,所以上面使用flag时加入了额外的判断,此处严格意义上应该再引入一个条件变量,每次打印完毕先让子线程获取锁去进入阻塞,另一种更简便的方法是在主线程中执行1s睡眠,保证子线程每次都能优先获取互斥锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int shareNum[]={1,2,3,4,5,6,7,8};
pthread_mutex_t mlock;//全局变量声明互斥锁指针
pthread_cond_t cond;//全局变量声明线程条件变量
void *printValue(void* arg){
do{
pthread_mutex_lock(&mlock);//子线程申请锁
pthread_cond_wait(&cond,&mlock);//如果得锁,交出互斥锁,等待主线程执行;
for(int i=0;i<8;i++){
printf("%d",shareNum[i]);
}
puts(""); //注意换行符是必须的,否则会堵在输入缓冲器没有输出
pthread_mutex_unlock(&mlock);//子线程释放锁
}while(1);
pthread_exit(NULL);
}
int main(){
pthread_t tid;
if(pthread_create(&tid,NULL,printValue,NULL)!=0){
perror("Pthread create failed!\n");
return -1;
}
if(pthread_cond_init(&cond,NULL)!=0){//线程条件变量初始化
perror("Cond Init Failed!\n");
return -1;
}
do{
sleep(1);//一秒打印一次,主线程谦让一秒,第一次必须先让子线程首先获取锁
pthread_mutex_lock(&mlock); //主线程申请锁
for(int i=0;i<4;i++){
int temp=shareNum[i];
shareNum[i]=shareNum[7-i];
shareNum[7-i]=temp;
}
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mlock);//主线程释放锁
}while(1);
pthread_join(tid,NULL);
return 0;
}
Linux进程间通信(IPC)
Linux是一个灵活的操作系统,能够应用在服务器、嵌入式设备,很多情况下需要不同的进程间共享资源、通信,进程间通信(Inter-Process Communication,IPC)是一个极其重要的内容,包括七种方式:无名管道、有名管道、信号、共享内存、消息队列、信号灯以及套接字socket(网络编程)。
无名管道(Pipe)
进程的空间分为用户空间和内核空间,内核空间在不同的进程中是可以共用的。无名管道就是在内核创建空间用于进程间的通信。有以下特点:
1. 只能用于具有亲缘关系的进程之间通信;
2.
半双工的通信模式,具有固定的读端和写端;
3.
管道可以看成是一种特殊文件,对它的读写类似文件IO,使用read、write方法。
无名管道创建函数:pipe() 1
2
3
4
5
6
7
8
9
10
11
12
13
14/****************************************
头文件:
#include <unistd.h>
函数原型:
int pipe(int pipefd[2]);
函数参数:
pipefd:包含两个元素的数组,pipefd[0]代表读端,管道的输出端;pipefd[1]代表写端,管道的输入端。
返回值:
成功:0
失败:-1
****************************************/
1. 简单读写:
因为一个进程已经默认打开了三个文件描述符:分别是0代表标准输入stdin
,1代表标准输出stdout
,2代表标准错误stderr
。因此使用管道创建了读端、写端的文件描述符为3和4,如果有其他文件描述符占用,该值会更大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(){
int pfd[2]={0};
if(pipe(pfd)<0){
perror("pipe error!\n");
return -1;
}
printf("%d %d\n",pfd[0],pfd[1]);//输出3、4
write(pfd[1],"Hello World",11);
char buf[32];
read(pfd[0],buf,11);
printf("%s\n",buf);
return 0;
}
2. 无名管道读写细节
当管道无数据,执行read时会进入阻塞状态;
1
2
3
4
5write(pfd[1],"Hello World",11);
char buf[32];
read(pfd[0],buf,11);
printf("%s\n",buf);
read(pfd[0],buf,11);//11已经被读完了,这里会阻塞管道默认最大空间是64K(65536个字节),写满了就进入阻塞;等到读出空余空间为4K(4096)时,才会执行写操作;
1
2
3
4
5char buf[65536]={0};
write(pfd[1],buf,65536);
printf("Hello\n");
write(pfd[1],"a",1); //超过空间,进入阻塞
printf("Hello\n");如果关闭了读端pipe[0],那么向管道写入数据是无效的,管道属于破裂状态,进程会收到内核传来的SIGPIPE信号(通常指向管道破裂)。
3. 管道应用:双进程循环输入和输出
下面代码实现了从父进程输入、从子进程打印,数据通过pipe传输;注意进程和线程的差别,系统会为进程分配额外的资源,虽然我们只定义了一个buf,但是子进程在复制父进程时会为buf开辟新的空间,因此不能像线程那样使用全局变量通信,而只能通过IPC共享某些信息。
第二个细节问题是这里在父子进程中无需像线程一样担心同步的问题,可以看见代码中父子进程都是不断执行的死循环,但是代码结果却能够交替工作,这也是因为CPU本身就支持多个进程并行执行(虽然这也是串行的假象);
第三个细节问题是write的时候不能是strlen(buf)而必须是strlen(buf)+1,strlen是不计算\0字符的,这样才能把fgets自动添加的\0写到管道中;如果\0没有写到管道中,那么每次管道的数据都是依靠“覆盖”进行的,例如第一次输入123456,第二次输入789,则第二次会输出789,因为没有找到结束符,789,因此要么把\0加入,要么每次打印前清空缓冲区。
第四个细节问题是管道是一种FIFO结构,边读、边写的情况下一般不会出现越界问题,因此定义32字符的读写也无需做额外的操作来保证它是否越界。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int main(){
char buf[32];//用于存放读写数据
int fd[2]={0}; //无名管道数组
if(pipe(fd)<0){
perror("pipe failed!\n");
return -1;
}
pid_t pid=fork();
if(pid<0){
perror("fork failed!\n");
return -1;
}
if(pid==0){//子进程
while(1){
if(strncmp(buf,"quit",4)==0)
break;
read(fd[0],buf,32);
printf("%s",buf);
}
exit(0);//退出
}
else{//父进程
while(1){
fgets(buf,32,stdin);
if(strncmp(buf,"quit",4)==0)
break;
write(fd[1],buf,32);//如果不想用32,可以使用strlen(buf)+1
}
wait(NULL);//等待回收子进程资源
}
return 0;
}
有名管道(FIFO)
无名管道只支持具有亲缘关系的进程进行通信,有名管道(FIFO,又称命名管道,Named Pipe)弥补了这个缺陷,支持两个无关的进程进行通信;此外有名管道可以通过路径名来标识,并且在文件系统中可见。
与无名管道类似,FIFO通过文件IO进行操作,遵循先进先出原则,不支持随机访问,故不支持lseek()
操作。
有名管道创建函数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/****************************************
头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:路径+文件名
mode:指定创建管道的访问权限,一般为三位8进制数
(0:无权限;1:执行;2:写;3:执行+写;4:读;
5:读+执行;6:读写;7:读写执行)
(三位:所有者、所有者的组、其他人)
返回值:
成功:0
失败:-1
****************************************/
1. 简单读写验证
1 |
|
2. 双进程实现读写显示
该代码用于创建读与写进程,写进程用于接收命令行输入内容,写入管道,读进程从管道读出数据并且显示在另一个终端的命令行上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//write.c 读命令行、写管道
int main(){
if(mkfifo("./fifo",0666)<0){ //0不能省略,这是标志八进制数必须的
if(errno==EEXIST){
printf("File Exist!\n"); //存在只报警告不perror
}
else{
perror("mkfifo failed!\n");
return -1;
}
}
int fd=open("./fifo",O_RDWR);
if(fd==-1){
printf("open error:%d!\n",errno);
return -1;
}
char buf[32]={0};
while(1){
fgets(buf,32,stdin); //从输入流按32字节写入buf
write(fd,buf,32); //buf内容写入管道fd
if(strncmp(buf,"quit",4)==0)
break;
}
printf("Write Done!\n");
close(fd);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26//read.c 读管道
int main(){
int fd=open("./fifo",O_RDWR);
if(fd==-1){
printf("open error:%d!\n",errno);
return -1;
}
char buf[32]={0};
while(1){
read(fd,buf,32); //从管道fd读数据在buf
if(strncmp(buf,"quit",4)==0)
break;
printf("%s\n",buf);
}
printf("Read Done!\n");
return 0;
}
3. 双进程实现cp命令功能
基于2修改,read进程接收文件名,将数据读入buf再写入管道文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//read_cp.c
int main(int argc,char*argv[]){
if(mkfifo("./fifo_cp",0666)<0){ //创建有名管道
if(errno==EEXIST){ //文件存在,忽略这个保持,仅打印提醒
printf("Fifo Exist!\n");
}
else{
perror("mkfifo failed!\n");
return -1;
}
}
int fd_fifo=open("./fifo_cp",O_WRONLY); //只写打开管道
int fd_file=open(argv[1],O_RDONLY,0666); //只读需要复制的文件
char buf[32];
ssize_t s; //ssize_t是有符号整数,size_t是无符号,避免负值错误
while(1){
s=read(fd_file,buf,32);//read返回ssize_t类型,表示读成功的字节数
if(s==0){
break; //没有读到字符了,退出循环
}
write(fd_fifo,buf,s);
}
close(fd_fifo);//关闭管道和文件
close(fd_file);
}
写文件进程:从read创建的管道中读取文件内容,读入缓冲区,写入新文件,复制完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//write_cp.c
int main(int argc,char*argv[]){
int fd_fifo=open("./fifo_cp",O_RDONLY); //只读打开管道
//创建新的文件名,只写、不存在则创建、存在则清空内容,创建权限666
int fd_newfile=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666);
char buf[32];
ssize_t s;
while(1){
s=read(fd_fifo,buf,32); //从管道读取之前复制的内容,读入缓存
if(s==0){
break;
}
write(fd_newfile,buf,s); //缓存数组写入文件
}
//关闭管道和文件
close(fd_fifo);
close(fd_newfile);
}
编译:gcc read_cp.c -o readcp gcc write_cp.c -o writecp
假设需要复制的文件为test.c
./readcp test.c ./writecp Mytest.c 验证:diff test.c Mytest.c
没有字符输出说明二者文件内容是一致的,复制效果成功。
信号
在单片机中,有一种机制称为中断,使得MCU无需时刻轮询等待任务触发,当有需要的任务发生时触发中断机制,MCU记录断点,进入中断处理任务后重新回到断点继续执行任务。
在软件层面,信号通信是对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程能够通过信号告知用户空间进程发生了哪些系统事件。
如果某个进程并未处于执行态,而有另外的进程向这个进程发送信号,信号会暂时被内核保存起来,直到其恢复执行;如果一个信号被进程设置为阻塞,则该信号的传递会被延缓,直至进程取消其阻塞状态才会被传递。
1. 常用信号
信号名 | 含义 | 默认操作 |
---|---|---|
SIGKIL | 立刻结束进程的运行,不能被阻塞、处理和忽略 | 终止进程 |
SIGALRM | 定时器到时时自动发出该信号 | 终止 |
SIGSTOP | 用于暂停一个进程,不能被捕捉和忽略 | 暂停进程 |
SIGTSTP | 用于暂停交互进程,用户可以键入SUSP字符(ctrl+z)发出该信号 | 暂停进程 |
SIGCHLD | 子进程状态改变,父进程会收到这个信号 | 忽略 |
SIGABORT | 用于结束进程 | 终止 |
SIGHUP | 在(正常或非正常)结束与用户终端连接时发出,通常用于告知同一个会话的各个作业与控制终端不再关联 | 终止 |
SIGINT | 终端驱动程序发送该信号并送到每一个前台进程,通常是用户键入了INTR字符(ctrl+c) | 终止 |
SIGQUIT | 与SIGINT类似,但一般控制字符为(ctrl+\) | 终止 |
SIGILL | 进程试图执行一条非法指令时出现的错误(可执行文件本身错误试图执行数据段、堆栈溢出 | 终止 |
SIGFPE | 致命算术运算错误时发出,例如浮点运算错误、溢出、除数为0 | 终止 |
2. 用户进程对信号的响应方式
- 忽略信号:对信号不做任何处理,除了两个信号不能被忽略或者捕捉,即SIGKILL(立刻终止进程)、SIGSTOP(停止或者暂停进程)。
- 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
- 执行缺省操作:在Linux中每种信号都内置了默认操作,用户进程可以直接按照默认操作进行响应。
3. 信号的发送
kill,给某个进程发信号(kill
-l可以查看支持的信号类型);raise,可以给当前进程发信号
1
2
3
4
5
6
7
8
9
10
11
12/****************************************
#include <signal.h>
int kill(pid_t pid, int sig); //给某个进程发信号
int raise(int sig); 给自身进程发信号
****************************************/
int main(){
kill(getpid(),SIGINT); //getpid获取当前进程,与raise是等效的
//raise(SIGINT);
while(1);
return 0;
}
4. 定时发送
alarm
是闹钟函数,会在进程中设计一个定时器,当定时器时间到达,内核会自动向进程发送SIGALARM
信号;pause
函数是将当前进程挂起,直到收到信号为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/****************************************
头文件:
#include <unistd.h>
函数原型:
unsigned int alarm(unsigned int seconds);
参数:
unsigned int seconds,代表需要定时的时间秒数
返回值:
如果调用此alarm()前已经设置过闹钟,那么返回剩余的定时时间;否则返回0,失败返回-1;
函数原型:
int pause(void); //收到信号前挂起进程
返回值:
当收到信号,返回-1,并且将errno设置为EINTR
****************************************/1
2
3
4
5
6
7
8
9
10
int main(){
alarm(3);
sleep(2);
printf("%d\n",alarm(5)); //打印1
pause(); //5秒后进程收到SIGALARM退出(输出闹钟)
return 0;
}
5. 信号处理
1 | /**************************************** |
ctrl+c触发SIGINT信号的三种处理方式,忽略、默认、自定义处理;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MyAction(int sig){
if(sig==SIGINT)
printf("My Action Stop!\n");
}
int main(){
//signal(SIGINT,SIG_IGN); //SIG_IGN代表忽略该信号
//signal(SIGINT,SIG_DFL); //SIG_DFL代表按默认方式(即终止进程)进行
signal(SIGINT,MyAction); //自定义函数处理
pause(); //收到信号默认退出
return 0;
}
6. 信号通信实例
- 售票员捕捉SIGINT信号(ctrl+c),向司机发送SIGUSR1信号,司机响应打印“gogogo”;
- 售票员捕捉SIGQUIT信号(ctrl+),向司机发送SIGUSR2信号,司机响应打印“stop the bus”;
- 司机捕捉SIGTSTP(ctrl+z),向售票员发出SIGUSR1信号,售票员打印“Please get off the bus”;
司机充当父进程,售票员充当子进程, 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
pid_t pid;
void Actiondo(int sig){
if(sig==SIGINT)
kill(getppid(),SIGUSR1); //向父进程发SIGUSR1
else if(sig==SIGUSR1) //父进程响应
printf("let's gogogo\n");
else if(sig==SIGQUIT)
kill(getppid(),SIGUSR2);//向父进程发SIGUSR2
else if(sig==SIGUSR2)
printf("stop the bus\n"); //父进程响应
else if(sig==SIGTSTP)
kill(pid,SIGUSR1); //父进程向子进程发SIGUSR1
}
void extra(int sig){
if(sig==SIGUSR1){ //SIGUSR1对父子含义不同,因此分开另外一个函数
printf("Please get off the bus\n"); //子进程响应
exit(0);
}
}
int main(){
pid=fork();
if(pid<0){
perror("fork failed!\n");
return -1;
}
if(pid==0){ //子进程,售票员
signal(SIGINT,Actiondo);
signal(SIGQUIT,Actiondo);
signal(SIGUSR1,extra); //只有收到司机的SIGUSR1才会执行退出;
signal(SIGTSTP,SIG_IGN);
}
else{ //父进程,司机
signal(SIGUSR1,Actiondo);
signal(SIGUSR2,Actiondo);
signal(SIGTSTP,Actiondo);
signal(SIGINT,SIG_IGN);
signal(SIGQUIT,SIG_IGN);
wait(NULL); //等待子进程退出
exit(0);
}
while(1); //确保程序不结束,父子进程都会存在
}
共享内存
共享内存是进程间通信最为高效的进程间通信方式,进程可以直接读写内存,而无需进行任何数据的拷贝;为了在多个进程间交换信息,内核专门留出了一块内存区,由需要访问的进程将其映射到直接的私有地址空间,然后进程可以直接读写这段空间而无需进行数据拷贝,同时这段内存由于和多个进程共享,因此也需要某些同步机制,例如互斥锁和信号量;
共享内存实现步骤
- 创建/打开共享内存
- 映射共享内存,将共享内存映射到进程的地址空间用于访问;
- 撤销共享内存的映射;
- 删除共享内存对象;
共享内存的创建
1 | /**************************************** |
1 | /**************************************** |
key值的组成:proj_id子序列号的ASCII(两位)+固定编号(01)+四位文件索引结点(inode,可以通过ls-i+文件名查看),查看结果转换成16进制的低四位作为key值组成部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(){
key_t key;
key=ftok("./abcd",'a'); //ls -i abcd:5506634(十六进制54064a)
if(key<0){
perror("ftok error");
return -1;
}
printf("%#x\n",key); //十六进制打印0x6101064a
int shmid;
shmid=shmget(key,128,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0){
if(errno==EEXIST)
shmid=shmget(key,128,0666);
else{
perror("shmget error");
return -1;
}
}
printf("%d\n",shmid);
return 0;
}
共享内存的映射和撤销映射
创建映射关系: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/****************************************
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存返回的标识号
shmaddr:指定共享内存的地址,一般传入NULL,让系统自动完成;
shmflg:SHM_RDONLY:共享内存只读;0(默认),共享内存可以读写;
返回值:
成功:映射后的内存地址
失败:-1
****************************************/1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/****************************************
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数:
int shmdt(const void *shmaddr);
参数:
shmaddr:指定共享内存的地址,可使用shmat返回的地址
返回值:
成功:0
失败:-1
****************************************/
1 | char *p=NULL; |
在共享内存被删除前,可以使用命令ipcs-m
查看(代码调用system("ipcs
-m"))Linux共享内存信息;
共享内存的删除
共享内存的删除通过函数shmctl
实现,该函数可以通过命令对共享内存的属性进行配置,如果需要配置属性,就需要使用结构体来配置;如果需要删除,则cmd使用IPC_RMID
,结构体传NULL即可;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/****************************************
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存段标识号
cmd:命令参数
buf:结构体,用于共享内存属性配置,删除传入NULL;
返回值:
成功:0或其他标识
失败:-1
****************************************/
1 | shmctl(shmid,IPC_RMID,NULL); |
信号灯
在共享内存中,写进程不断地写到共享内存,读进程不断循环读共享内存,显然这种方式是不够理想的;如果需要读写配合,就需要用到和线程同步机制信号量一样的进程通信方式,在IPC里这种方式同样被称为信号灯(Semaphore,也称信号量)。
信号量最早由荷兰学者引入,基本操作分为两个原子操作(PV操作): - P操作(荷兰文proberen,尝试):表示信号量值大于0,P操作可以继续执行,信号量-1,直到信号量降为0,进程进入阻塞等待;
- V操作(荷兰文verhogen,释放):表示释放信号量,信号量+1,如果有等待的就从进程或者线程中选择一个来唤醒执行;
Linux中信号量通常通过信号量API来实现,分为System V信号量和POSIX信号量; - POSIX信号量:在线程那一节我们实现的信号量就是通过POSIX信号量接口实现的,POSIX信号量的API更加简洁且现代化,考虑到了线程安全问题,System V信号量可能需要额外的同步机制来保证线程的安全;
- System V信号量:System V信号量是一种比较早期的信号量,最大的特点是它支持信号灯集,一次可以创建或者操作一组的信号量(POSIX只能操作单一的信号量),其也提供了比较复杂的API操作,如调整信号量的值,获取信号量状态等。
信号灯可以是二值信号灯或者计数信号灯:二值信号灯就像是互斥锁的结构,资源可用为1,不可用为0;计数信号灯则是以资源数为计数量,资源降为0则等待直到有资源可用;
System V信号灯
创建一个信号灯集(可以指定信号灯集有多少个信号灯)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/****************************************
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数:
int semget(key_t key, int nsems, int semflg);
参数:
key:和信号灯关联的key值
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限(常用IPC_CREAT|0666)
返回值:
成功:信号灯集的标识ID
失败:-1
****************************************/对信号灯进行初始化 和共享内存的
shmctl
函数很类似,semctl
可以根据命令参数对信号灯进行设置和操作,包括初始化;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/****************************************
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semctl(int semid, int semnum, int cmd, ...); //初始化设置信号灯参数SETVAL,
参数:
semid:信号灯集标识的ID
semnum:信号灯集的下标(第一个信号灯0,第二个为1,以此类推)
cmd:命令参数
返回值:
成功:返回0或者其他标识
失败:-1
****************************************/PV操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/****************************************
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数:
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
semid:信号灯集标识
sops:配置结构体指针,结构体细节如下(在头文件定义,声明即使用无需重新定义):
struct sembuf{
unsigned short sem_num; //下标,代表哪个信号灯
short sem_op; //P操作为-1(其他负数也是每次减去,不能减就阻塞),V操作为1(其他正数就是释放加多少),代表资源变化;如果为0,进程阻塞等待信号量为0继续执行,即等待足够的PV操作;
short sem_flg; //标志位,SEM_UNDO代表如果进程PV操作时异常结束,系统会还原信号量(避免死锁);0代表没有特殊行为;
};
nsops:操作多少个信号量
返回值:
成功:返回0;
失败:-1;
****************************************/信号灯的删除 和初始化一致,信号灯的删除一样通过
semctl
函数来实现,命令参数为IPC_RMID
:1
semctl(semid,0,IPC_RMID); //删除信号灯集ID为semid,下标为0的信号灯;
一个创建信号灯集、初始化信号灯、进行P操作、删除信号灯的例子如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
union semun{ //初始化信号灯函数semctl(semid,下标,SETVAL,结构体)的结构体;
int val; //代表要初始化的信号量值
};
int main(){
key_t key;
key=ftok("./abcd",'c');
if(key<0){
perror("ftok error"); //key值获取同共享内存
return -1;
}
int semid=semget(key,2,IPC_CREAT|IPC_EXCL|0666); //创建信号灯集,灯集内有2个信号灯
union semun mysem; //实例化初始化的结构体
if(semid<0){ //灯集有直接用,没有就创建
if(errno==EEXIST)
semid=semget(key,2,0666);
else{
perror("semget error");
return -1;
}
}
else{ //只有新建的才会初始化,否则会沿用上次进程的信号灯(如果上次没有删除信号灯集)
mysem.val=10;
semctl(semid,0,SETVAL,mysem);
}
printf("%d\n",semctl(semid,0,GETVAL,mysem)); //打印0下标信号灯的值
struct sembuf mybuf; //sembuf是信号灯头文件定义的结构体,直接声明使用;
mybuf.sem_num=0;//下标,哪个信号灯
mybuf.sem_op=-1; //消耗资源,P操作
mybuf.sem_flg=0; //无特殊行为
semop(semid,&mybuf,1); //通过结构体执行PV操作,1代表执行PV的信号灯数量为1
printf("%d\n",semctl(semid,0,GETVAL,mysem));
semctl(semid,0,IPC_RMID); //可以通过ipcs -s查看系统信号灯数量,如果删除就没有了,注释就会存在;
return 0;
}
共享内存+信号灯应用:双进程实现读写打印
将本节介绍的信号灯加入到共享内存中,要求实现写一次、打印一次字符串的效果,思路和线程同步那节信号量+双线程循环输入和输出是一致的:首先在不使用信号量时,写进程可以不断通过输入将字符写入共享内存,读进程不断从共享内存中读和打印;此时加入了一个信号量,写进程读取完毕才能释放资源,使读进程打印,实现了一次输入、一次输出的过程;但此时仍存在问题,在读进程打印时,由于写进程的运行是无条件的,因此此时如果输入字符就会立马覆盖到内存中,最后的结果表现为如果单次输入字符数量大于fgets设定的大小,那么后面多余部分就会覆盖前面部分;这种情况下应该增加一个信号量,该信号量只有等打印完了才能释放给写进程,实现严格的互斥运行关系。
注意一个调试问题:在改变信号灯集中信号量的数量时,假如代码是无错误的,通过ipcrm
-m、ipcrm
-s删除了共享内存、信号还是无效,大概由于操作系统仍然保留原始信号,因此没有真正执行semget(key,2,IPC_CREAT|IPC_EXCL|0666),这种情况需要重新指定key值最为方便;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74//shmwrite.c 写进程,将输入数据写到共享内存
union semun{
int val;
};
int main(){
key_t key=ftok("./abcde",'f');
if(key<0){
printf("ftok failed\n");
return -1;
}
int shmid;
int semid;
semid=semget(key,2,IPC_CREAT|IPC_EXCL|0666); //信号灯集:数量2
union semun mysem;
shmid=shmget(key,128,IPC_CREAT|IPC_EXCL|0666);//共享内存
if(shmid<0){
if(errno==EEXIST)
shmid=shmget(key,128,0666);
else{
perror("shmget error");
return -1;
}
}
if(semid<0){
if(errno==EEXIST){
semid=semget(key,2,0666);
if(semid<0){
perror("Reuse ERROR");
return -1;
}
}
else{
perror("semget error");
return -1;
}
}
else{
mysem.val=0; //信号量1初始值设为0
semctl(semid,0,SETVAL,mysem);
mysem.val=1; //信号量2初始值设为1,确保刚开始时写进程能运行;
semctl(semid,1,SETVAL,mysem);
}
char*p=NULL;
p=shmat(shmid,NULL,0);
if(p==(char*)-1){
perror("shmat error");
return -1;
}
struct sembuf mybuf;
struct sembuf mybuf1;
mybuf.sem_num=0;
mybuf.sem_op= 1;
mybuf.sem_flg=0;
mybuf1.sem_num=1;
mybuf1.sem_op=-1;
mybuf1.sem_flg=0;
while(1){
semop(semid,&mybuf1,1);
if(strncmp(p,"quit",4)==0)
break;
fgets(p,32,stdin);
semop(semid,&mybuf,1);
}
}
1 | //shmread.c 读进程,将共享内存里的数据打印到读终端 |
消息队列
消息队列是一种进程间异步通信的方法,允许一个进程向另外一个进程发送数据(消息),这些信息存放在队列中并且由操作系统内核进行管理,其是一种FIFO结构,根据发送顺序被读走,此外消息队列一个特点是发送方和接收方都能够根据类型进行发送和接收。
消息队列的创建、发送、读取信息、删除队列等与信号量、共享内存基本相似;
1. 消息队列的创建
1 | /**************************************** |
2. 发送消息
1 | /**************************************** |
3. 接收信息
1 | /**************************************** |
4. 删除消息队列
1 | /**************************************** |
简单实例
1 |
|
如果需要查看队列是否正常删除,可以通过ipcs -q查看;