在前面的文章中(Linux编程基础——多线程),简单对Linux中的多线程进行了介绍,包括pthread、信号量与互斥锁,本文将对Linux编程中的多任务间通信与同步技术进行相对完整的补充。
在Linux中有两种多任务实现手段:进程和线程。
Linux中主要提供了以下的一些方式来实现多任务通信和同步:
其中,前5种主要用于多任务间通信,后2种主要用于多任务间同步。
信号(Signal)是Linux操作系统中用于进程间通信和进程内部通信的一种机制。信号是由内核向进程发送的一种异步通知(类似中断),表示某个事件已经发生或者某个条件已经满足。每个信号都对应一个唯一的整数信号值,例如SIGINT表示中断信号。
Linux中的型号包括两种类型:同步信号和异步信号。
信号涉及头文件
,将信号都定义为整数。
信号名称 | 信号定义 |
---|---|
SIGINT | 终端中断,终止进程的中断信号,通常由CTRL+C触发 |
SIGTERM | 终止进程的请求信号,通常由kill命令发送 |
SIGKILL | 停止进程,强制终止进程的信号,无法被忽略、阻塞或处理,通常用于终止僵尸进程和崩溃的进程 |
SIGSTOP | 停止执行,暂停进程的信号,无法被忽略、阻塞或处理,通常由CTRL+Z触发 |
SIGTSTP | 终端停止信号,暂停进程的信号,可以被忽略、阻塞或处理,通常由CTRL+Z触发 |
SIGCONT | 如果被停止则继续执行,恢复进程的信号,通常用于从暂停状态中恢复进程 |
SIGHUP | 系统挂断,终端挂起或断开的信号,通常用于重新读取配置文件或重启进程 |
SIGUSR1 | 自定义信号1,可以用于进程间通信 |
SIGUSR2 | 自定义信号2,可以用于进程间通信 |
从信号发送到信号处理函数执行完毕的全过程称为信号的生命周期。可以分为三个重要的阶段,主要事件:信号产生;信号在进程中注册;信号在进程中注销;信号处理函数执行完毕。
kill()
或sigqueue()
等。)#include
#include
int kill(pid_t pid, int signo);
其中signo为要发送的信号值。调用成功返回0,否则返回-1。
#include
int sigqueue(pid_t pid, int signo, const union sigval value);
其中,signo为要发送的信号类型,value是一个union类型,用于传递信号的附加信息。
用户进程对于信号的响应可以有以下三种处理方式:
void (*signal(int sig, void (*func)(int)))(int);
//可替换为以下方式理解
typedef void sign(int);
sign *signal(int, handler *);
关于signal函数的使用:
void func(int)
;// 对信号SIGINT的响应函数
void sig_int(int sig) {
printf("Received signal %d\n", sig);
// 进行信号处理
}
// 注册信号
signal(SIGINT, sig_int);
当收到Ctrl+C组合键时,将会打印信号“Received signal 2”。
管道(“|”)是Linux中一种非常强大的特性,是进程间通信(IPC,Inter Process Communication),用于将一个命令的输出作为另一个命令的输入。通过使用管道,可以将多个命令串联在一起,从而实现更多的复杂操作。
无名管道(PIPE),可用于具有亲缘关系进程间的通信。
有名管道(FIFO),除具有管道所具有的功能外,还允许无亲缘关系进程间的通信。
关于(无名)管道的特性:
管道是由系统调用pipe()
函数创建,具体使用操作包括创建、写、读、关闭。
步骤1:创建管道
#include
int pipe(int pipefd[2]);
pipe函数用于创建一对无名的管道文件描述符。
//用于保存无名管道的文件描述符
int pipefd[2];
if(pipe(pipefd)<0)
{
//创建管道失败
}
步骤二:写管道
#include
char buf[BUFSIZ];
//将buf中内容写入到管道
write(pipefd[1], buf, BUFSIZ);
步骤三:读管道
//从管道中读取内容到buf
rcount = read(pipefd[0], buf, BUFSIZ);
可以看到,通过创建之后的管道文件描述符进行管道的操作。
步骤四:关闭管道
在管道操作完成之后,通过close函数关闭管道。
close(pipefd[0]);
close(pipefd[1]);
FIFO,也成为有名管道(Named Pipe),是Linux中一种特殊类型的文件。它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。因此,即使与FIFO创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信。
另外,从FIFO的命名来看,可知道FIFO管道中数据的方式为,先进先出,(First In First Out),即读从开始处返回,写则添加到末尾。
FIFO的操作包括,创建、打开和关闭、写与读、删除等操作。
使用mkfifo命令创建FIFO文件。
int mkfifo(const char *pathname, mode_t mode);
创建成功返回0。模式方式有:
#include
//创建FIFO之前,通过access函数来检查文件是否存在,或权限
int access(const char *pathname, int mode);
//当文件检查失败失败则调用mkfifo创建
if(access(FIFO_NAME, F_OK)==-1)
{
res = mkfifo(FIFO_NAME, 0777);
}
创建FIFO文件之后,可以使用文件IO操作打开FIFO,语法为 fd = open(pathname, flags)
,其中pathname为FIFO文件的路径名,flags为打开文件的方式。
打开成功后,可以使用read和write函数向FIFO中写入和读取数据。
关闭FIFO使用close函数,语法为 close(fd)
在文件中,则使用write(pipe_fd, buf, BUFFER_SIZE
来写入。
命令行操作,可以使用cat和echo命令向FIFO中写入数据,语法为echo "data" > filename
和cat file.txt > filename
。
对于管道的另一端,则通过read函数读取FIFO中数据。
在命令行操作中,可以使用tail和cat命令从FIFO中读取数据,语法为tail -f filename
和cat filename
。
如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志的读操作来说则返回-1。
共享内存是一种高效的进程间通信方式。两个不同的进程A、B共享内存的意义是通过映射之后,在进程地址空间访问同一块物理内存。A、B之间可以及时看到共享内存中数据的更新。
共享内存的一种实现方式是通过mmap()
系统调用。通过mmap()
系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read、write等操作。mmap系统调用配合使用的系统调用还有munmap()
、msync()
等,函数原型定义如下:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//映射解除,解除映射关系后,对原来映射地址的访问将导致段错误
int munmap(void *addr, size_t len);
//实现磁盘文件内容与共享内存区内存同步,保持一致
int msync(void *addr, size_t len, int flags);
一般来说,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()
后才执行该操作。可以通过msync()实现磁盘上文件内容与共享内存区的内容一致。
除系统调用mmap以外,Linux中还引入了System V共享内存。
内存专门留出了一块共享内存区域,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中。每个内存区域都有一个标示符(shmid),进程通过这个标示符访问内存区域。
#include
int shmget(key_t key, size_t size, int flag);
void *shmat(int shmid, const void *addr, int flag);
void shmdt(void *addr);
分配一段内存空间来存储共享内存对象,可以使用shmat()函数将内存挂接到当前进程的地址空间中。
对挂接到进程地址空间的内存进行读写操作,可以使用memcpy()等相关函数。
使用shmdt()函数将共享内存对象从进程地址空间中分离,使其不再被当前进程使用。
最后使用shmctl()函数对共享内存对象进行进一步的控制,如删除共享内存对象、查询共享内存信息等。
需要注意的是,在使用共享内存时需要对共享内存进行加锁保护,避免多个进程同时对共享内存进行读写产生竞争和错误。此外,在多进程环境下,使用共享内存需要使用信号量等同步机制来保证进程间的同步和互斥。