进程间通信
进程之间的内存是隔离,如果多个进程之间需要进行信息交换,常用的方法有:
- 套接字
- 管道
- 共享内存
- 消息队列
- 信号量
匿名管道
匿名管道是位于内核的一块缓冲区,用于进程间通信。
创建匿名管道的系统调用为pipe。
在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输。
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd:用于返回指向管道两端的两个文件描述符。
pipefd[0]指向管道的读端。pipefd[1]指向管道的写端。
两个进程通过一个管道只能实现单向通信,比如,父进程写子进程读,有时候需要子进程写父进程读,就必须打开另一个管道。
管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。
可以父进程for两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。
当开启管道时,父进程会创建两个struct file结构体用于管道的读写操作,并为两者各自分配一个文件描述符。它们的private data属性指向同一个结构体,由后者管理对管道缓冲区的读写。
private_data指向struct pipe_inode_info类型的结构体。
有名管道(FIFO)
有名管道可以用于任何进程之间的通信。但只能单向通信。
char *pipe_path = "/tmp/myfifo";
mkfifo(pipa_path, 0664);
有名管道使用完成后,应该通过unlink清除,这个函数只能调用一次。
内核为每个进程被打开的FIFO专用文件维护一个管道对象。当进程通过FIFO交换数据时,内核会在内部传递所有数据,不会将其写入文件系统,因此管道文件大小始终为0。
共享内存
shm_open可以开启一块内存共享对象,我们可以像使用一般文件描述符一般使用这块内存对象。
shm_unlink()移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源。
mmap()。
父进程在创建一个内存共享对象并将骑映射到内存区后,子进程可以正常读写该内存区,并且父进程也能看到更改。
char *share;
// 创建一个共享内存对象
char shm_name[100] = {0};
sprintf(shm_name, "/letter%d", getpid());
int fd;
fd = shm_open(shm_name, O_RDWR|O_CREAT, 0644);
if(fd < 0){
perror("shm_open");
exit(EXIT_FAILURE);
}
//设置共享内存对象大小
ftruncate(fd, 1024);
//内存映射
share = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED,fd,0);
if(share == MAP_FAILED){
perror("mmap");
exit(EXIT_FAILURE);
}
//映射完成之后,关闭fd连接
close(fd);
//使用内存映射,实现进程间通讯
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(EXIT_FAILURE);
}
if(pid == 0){
strcpy(share, "你是个好人\n");
printf("新学员%d完成回信\n",getpid());
}else{
waitpid(pid, NULL, 0);
printf("%s\n",share);
//释放映射区
munmap(share, 1024)
}
// 释放共享内存对象
shm_unlink(shm_name);
管道在内核里,所以管道内存非常小,而共享内存的内存很大,并不是存到磁盘里,因为磁盘速度非常慢。
临时文件系统
Linux的临时文件系统(tmpfs)是一种基于内存的文件系统,它将数据存储在RAM或者在需要时使用部分交换空间(swap)。
tmpfs访问速度快,但因为存储在内存,重启后数据清空,通常用于存储一些临时文件。
可以通过df -h查看当前操作系统已经挂载的文件系统。
共享内存对象在临时文件系统中的表示位于/dev/shm目录下。
最大子数组和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ret = nums[0];
int tempSum = nums[0];
for(int i=1; i<nums.size(); i++){
if(tempSum < 0){
tempSum = nums[i];
}else{
tempSum += nums[i];
}
ret = max(ret, tempSum);
}
return ret;
}
};
非父子进程通过消息队列通信
创建两个进程:生产者和消费者,前者从控制台接收数据并写入消息队列,后者从消息队列接收数据并打印控制台。
struct timespec time_info;
char write_buf[100];
while(1){
memset(write_buf, 0, 100);
ssize_t read_count = read(STDIN_FILENO, write_buf. 100);
clock_gettime = (0, &time_info);
time_info.tv_sec += 5;
if(read_count == -1){
perror("read");
continue;
}else if(read_count == 0){
//ctrl + d
printf("EOF,exit...\n");
char eof = EOF;
mq_timedsend(mqdes, &eof, 1, 0, &time_info);
break;
}
mq_timedsend(mqdes, write_buf, strlen(write_buf), 0, &time_info);
}
close(mqdes);
我们可以设置消息队列的模式为O_RDWR,使它可以用于收发数据,从技术上讲,单条消息队列可以用于双向通信,但这会导致消息混乱,无法确定队列中的数据是本进程写入的还是读取的,因此,不会这么做,单条消息队列只用于单向通信。
信号
信号是一种用于通知进程发生了某种事件的机制。
信号可以由内核、其它进程或者命令行工具发给目标进程。
Linux系统中有多种信号,每种信号都用一个唯一的整数值来表示,如常见的信号包括:
- SIGINT(2):这是用于在终端下按下Ctrl+c时发送给前台进程的信号,通常用于请求进程终止。
- SIGKILL(9):这是一种强制终止进程的信号,会立即终止目标进程,且不能被捕获或忽略。
- SIGTERM(15):这是一种用于请求进程终止的信号,通常由系统管理员或其他进程发送给目标进程。
- SIGUSR1、SIGUSR2是用户自定义的信号,可以由应用程序使用。
- SIGSEGV(11):这是一种表示进程非法内存访问的信号,通常是由于进程尝试访问未分配的内存或者试图执行非法指令而导致的。
- SIGALRAM(14):这是一个定时器信号,通常用于在一定时间间隔后向目标进程发送信号。
每种信号都有其特定的含义和行为,进程可以通过注册信号处理函数来捕获信号并执行相应的操作,例如终止进程、忽略信号等。
如果想查看所有的Linux信号,执行kill- l指令。
可以通过sinal系统调用注册信号处理函数
sighanlder_t signal(int signum, sighanlder_t handler);
线程
Linux中的线程是指轻量级的执行单元,相比于进程,具有以下特点:
- 进程是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程是进程内的一个执行单元,它们共享相同的地址空间和其他资源,包括文件描述符、信号处理等,每个线程都有自己的栈空间。
- 由于共享地址空间和数据段,同一进程的多线程之间进行数据交互比进程间通信方便很多,但也由此带来线程同步问题。
- 同一进程的多线程共享大部分资源,除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多,所以多线程比多进程更适合高并发。
线程操作相关函数来源于pthread共享库。
线程终止
- 线程函数执行return语句。
- 线程函数内部调用pthread_exit()函数
- 其它线程调用pthread_cancel()函数
#include <pthread.h>
void pthread_exit(void *retval);
结束调用该方法的线程,并返回一个内存指针用于存放结果。
当某个线程调用pthread_exit方法后,该线程会被关闭(相当于return)。
线程可以通过retval向其它线程传递信息,retval指向的区域不可以放在线程函数的栈内。其它线程如果需要获取这个返回值,需要调用pthread_join方法。
如果父线程先结束,会强制杀死子线程。
常见的锁机制
- 互斥锁:保证同一时刻只有一个线程能执行临界区的代码。
- 读写锁:允许多个读者可以访问共享数据,但写者要互斥访问。
- 自旋锁:在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景。
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&counter_mutex);
// 临界区代码
pthread_mutex_unlock(&counter_mutex);
读写锁里,只要有一个读操作持有读锁,写操作就无法获得写锁,写操作将会阻塞,直到所有读锁释放。
而多个线程可以同时获得读锁。
写饥饿
读写锁的写饥饿问题是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁从而不断推出写线程的执行。
这种情况通常出现在读操作远多于写操作时。
Linux提供了可以修改的属性pthread_rwlockattr_t,默认情况下,属性中指定的策略为“读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。
可以尝试将策略更改为写优先,当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长。
自旋锁
在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,避免多个处理器同时访问共享资源。
自旋锁相对于其它锁的优点是,它们在锁被占用时会持续检查锁的状态(即自旋),而不是让线程进入休眠,这使得自旋锁在等待时间非常短的情况下非常有效,避免了线程上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销,不能在用户空间使用。
restrict关键字
restrict用于修饰指针,作用是告诉编译器,被修饰的指针是编译器所知的唯一一个可以在其作用域内用来访问指针所指向的对象的方法。
这样一来,编译器可以放心地执行代码优化,因为不存在其他的别名(即其他指向同一内存区域的指针)会影响到这块内存的状态。
条件变量
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
//初始化互斥锁
static pthread_mutex_t mutex - PTHREAD_MUTEX_INITIALIZER;
//初始化条件变量
static pthread_cond_t cond - PTHREAD_COND_INITIALIZER;
void *producer(void *arg){
int item = 1;
while(1){
//获取锁
pthread_mutex_lock(&mutex);
//如果缓冲区写满 使用条件变量暂停当前线程
if(count == BUFFER_SIZE){
pthread_cond_wait(&cond, &mutex);
}
buffer[count++] = item++;
pthread_cond_signal(&cond);
//释放锁
pthread_mutex_unlock(&mutex);
}
}
void *comsumer(void *arg){
while(1){
//获取锁
pthread_mutex_lock(&mutex);
//如果缓冲区写满 使用条件变量暂停当前线程
if(count == 0){
pthread_cond_wait(&cond, &mutex);
}
printf("%d\n",buffer[--count]);
pthread_cond_signal(&cond);
//释放锁
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t producer_thread,comsumer_thread
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&comsumer_thread, NULL, comsumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(comsumer_thread, NULL);
return 0;
}