目录
补充内容
1、多个执行流(进程或线程)都能看到的资源叫做共享资源
2、对共享资源的访问本质都是通过代码进行的,进/线程的代码有 会 或 不会 访问共享资源之分
3、会访问共享资源的某段代码称为临界区,不会访问共享资源的某段代码称为非临界区,进出临界区需要加解锁操作(加解锁操作是因为多线程编程的日益兴起而引出的新概念,用来保证同时只有一个线程可以访问某一份共享资源,即线程间互斥)
4、临界区(某段代码)访问某部分(一个或多个)共享资源时这部分共享资源称为临界资源
5、对共享资源的访问方式分为 整块访问 和 分块访问:
- 整块访问:对整个共享资源加锁,确保在某一时刻只有一个线程或进程可以访问共享资源的任何部分(vip影院)
struct SharedResource {
int data1;
int data2;
// 其他成员
};
SharedResource resource;
std::mutex resource_mutex;
void access_resource() {
std::lock_guard<std::mutex> lock(resource_mutex); // 加锁保护整个共享资源(SharedResource 结构体),此时共享资源是一个整体,访问 data1 和 data2 的代码段都是临界区
// 访问和修改整个共享资源资源
resource.data1 = 10;
resource.data2 = 20;
//std::lock_guard<std::mutex> lock(resource_mutex) 创建了一个互斥锁 lock,并且在函数作用域结束时自动释放互斥锁。通过这种方式,在 lock 的生命周期内,只有一个线程能够进入临界区访问和修改 SharedResource 结构体
//先看看吧std::lock_guard<std::mutex> lock(resource_mutex)到后面才会理解
}
- 分块访问:对共享资源的不同部分分别加锁,允许多个线程或进程同时访问不同部分的资源(普通影院)
int shared_var1;
int shared_var2;
std::mutex mutex1;
std::mutex mutex2;
void access_var1() {
std::lock_guard<std::mutex> lock(mutex1); // 加锁保护 shared_var1
// 访问和修改 shared_var1
shared_var1 = 10;
}
void access_var2() {
std::lock_guard<std::mutex> lock(mutex2); // 加锁保护 shared_var2
// 访问和修改 shared_var2
shared_var2 = 20;
}
信号量
基本概念:信号量是一个计数器,用来控制进程或线程对共享资源的访问
- 根据实际情况将信号量初始化时的值等于1:此时可以实现进程或线程互斥
- 根据实际情况将信号量初始化时的值大于1:此时可以实现进程或线程同步
- 如果有互斥锁,就令当别论
信号量的分类:
- 二元信号量(整型信号量):处理整块访问(大于0可访问,等于0不可访问)
//某计算机系统中有一台打印机
int S = 1; //初始化整型信号量S,表示当前系统中可用的打印机资源数
- 多元信号量(记录型信号量):处理多块访问
/*记录型信号量的定义*/
typedef struct{
int value; //剩余资源数
struct process *L; //等待队列
}semaphore;
问题:信号量可以是全局变量吗?
解释:不能,①因为全局变量不能被所有进程看到(A进程申请一个全局生命周期的信号量,那么其它与A进程没有“血缘”关系的进程就不可能看到这个信号量,更别提当其它进程访问临界资源时该信号量能干什么事了)②信号量作为全局变量时的++或--,不是原子的(++或--在汇编层面涉及多条汇编指令,不能用一条汇编指令直接完成,可能会被中断)
补充:信号量也应该是一个共享资源,在使用它控制进程或线程对临界资源的访问的同时,还应保证信号量本身不会被修改(共享资源可以被修改但我们不想让它被随意的修改,因为信号量的大小应该是和可用资源的数量是对应的,信号量的大小不应大或小于实际可用资源的数量),保证信号量本身不会被随意修改的方式叫做PV操作(由OS负责实现对信号量的修改操作,操作者只需要对设定的信号量初始化即可,操作信号量时,也是调用OS提供的系统调用接口)
注意事项:信号量的设定应该以实际为标准,而不是人为随意设定的(实际存在的可访问的资源的数量进行设定的)
结论:用户对信号量的操作只涉及 初始化信号量 和 调用修改信号量两个系统调用接口
PV操作
- P操作:也称为“down”操作,使得信号量减1。如果信号量的值小于0,进程/线程将被阻塞,直到信号量的值大于或等于0
- V操作:也称为“up”操作,使得信号量加1。如果信号量的值小于或等于0,一个被阻塞的进程/线程将被唤醒
- OS会提供与PV操作相关的系统调用接口
//当sem大于1时,下面代码实现的是 进程间同步 当然也可以讲申请进程改为申请线程实现 线程间同步
//但是此时共享资源没有互斥锁的保护,会存在数据不一致问题
#include <iostream>
#include <thread>
#include <semaphore.h>
sem_t sem;//申请信号量
void task(int id) {
sem_wait(&sem); // P操作
// 临界区
std::cout << "Task ID: " << id << std::endl;
sem_post(&sem); // V操作
}
int main() {
sem_init(&sem, 0, 1); // 初始化信号量,初始值为1
std::thread t1(task, 1);
std::thread t2(task, 2);
t1.join();
t2.join();
sem_destroy(&sem); // 销毁信号量
return 0;
}
加解锁
基本概念:用于线程互斥,确保多个线程不会同时访问同一个临界资源,常用加解锁机制是互斥锁
-
互斥锁(mutex):用来保护临界资源的原语,确保线程间互斥
- 加锁:一个线程在进入临界区之前要调用加锁接口,如果互斥锁已经被另一个线程“持有”,则该线程将被阻塞,直到被别人“持有”的互斥锁被释放
- 解锁:一个线程在离开临界区时调用解锁接口释放互斥锁,从而使其它被阻塞的线程有机会获得互斥锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;//定义一个全局的互斥锁 mtx,用于保护临界区
//线程执行的函数 print_thread_id 接受一个整型参数 id,表示线程的编号
void print_thread_id(int id) {
mtx.lock();//尝试获取互斥锁。如果互斥锁已经被其他线程持有,则当前线程会被阻塞,直到互斥锁被释放
// 临界区代码(mtx.unlock()前的都是)
std::cout << "Thread ID: " << id << std::endl;
mtx.unlock();//释放互斥锁,使其他被阻塞的线程有机会获取互斥锁
}
int main() {
std::thread t1(print_thread_id, 1);//创建一个线程t1,并执行print_thread_id函数,传递参数 1
std::thread t2(print_thread_id, 2);//创建一个线程t2,并执行print_thread_id函数,传递参数 2
t1.join();//等待线程 t1 执行完毕
t2.join();//等待线程 t2 执行完毕
return 0;
}
结论:①通过将信号量设置为1可以实现进程或线程间互斥,将信号量设置为大于1的数可以实现进程或线程间同步②通过加解锁操作可以实现线程同步时对共享资源的互斥访问③进程不能使用互斥锁
总结
信号量函数(了解)
semget函数(获取标识符)
函数原型:int semget(key_t key, int num_sems, int sem_flags);
包含头文件: <sys/types.h> 和 <sys/ipc.h> 和 <sys/sem.h>
参数:用于标识信号量集的键值(
ftok
函数生成),指定要创建的信号量集中的信号量个数,指定创建的信号量的权限标志,通常是一个权限掩码(比如IPC_CREAT | 0666
)返回值:获取成功返回信号量集的标识符,失败返回-1
功能:获取一个信号量集的标识符
注意事项:如果指定的键值对应的信号量集已经存在,则
semget
函数会返回该信号量集的标识符,如果指定的键值对应的信号量集不存在,并且指定了IPC_CREAT
标志位,semget
函数会创建一个新的信号量集
semctl函数(获取结构体信息)
函数原型:int semctl(int sem_id, int sem_num, int cmd, ...);
包含头文件: <sys/types.h> 和 <sys/ipc.h> 和 <sys/sem.h>
参数:信号量集的标识符(semget的返回值),指定要操作的信号量在信号量集中的索引(通常为0即对整个信号量集进行操作),指定要执行的操作(比如
GETVAL
、SETVAL
等),可选参数(根据cmd
的不同可能需要提供额外的参数)返回值:对于不同的
cmd
命令,返回值的含义可能不同,通常情况下,成功执行时返回一个整数值,表示操作成功或者返回的信息。如果失败,返回-1,并设置全局变量errno
来指示错误原因功能:获取信号量集的信息、设置信号量的值等
注意事项:暂无
semop函数(执行操作)
函数原型:int semop(int sem_id, struct sembuf *sem_ops, size_t num_ops);
包含头文件: <sys/types.h> 和 <sys/ipc.h> 和 <sys/sem.h>
参数:信号量集的标识符(semget的返回值),指向
sembuf
结构体数组的指针,每个sembuf
结构体描述一个要执行的操作,指定要执行的操作的数量(即sem_ops
数组中的元素个数)返回值:操作成功返回0,操作失败返回-1
功能:对信号量集执行操作的函数,通常用于进行PV操作,实现进程之间的同步
注意事项:暂无
显示系统中当前使用的信号量集信息指令:ipcs -s
删除指定信号量集指令:ipcrm -s semid(某个信号量集的标识符)
信号
信号量和信号没有关系
基本概念:一种向指定进程发送特定事件的方式,进程接收信号后要对信号进行识别和处理
注意事项:
1、信号分为普通信号(Linux中的1~31个信号)的实时信号(Linux中的34~64个信号)
2、信号的产生是异步的(进程正在执行突然间可能就会出现一个信号)
3、进程对信号有三种处理方式,分别为默认动作、忽略动作、自定义处理(对信号进行捕捉)
4、信号是用位图保存起来的,发送信号就是OS修改指定进程PCB中存放的信号的位图
unit32_t signals;
0000 0000 0000 0000 0000 0000 0000 0000 0001//此时出现了1号
0000 0000 0000 0000 0000 0000 0000 0000 1000//此时出现了4号
5、真正向进程发送信号的是OS
signal函数
函数原型:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
包含头文件:<signal.h>
参数:signum是待处理信号的编号,handler是指向处理该信号函数的一个函数指针
返回值:返回一个指向信号处理函数的函数指针,若未设置处理函数,则返回
SIG_ERR
功能:设置对特定信号的处理方式(自定义处理信号函数)
注意事项:
1、传递
SIG_IGN
给handler则
将信号处理方式设置为忽略该信号,传递SIG_DFL
给handler则
将信号处理方式设置为默认操作2、因为第二个参数是函数指针,因此信号处理函数既可以是一个信号对应一个符合条件的回调函数,也可以是多个信号共用一个信号处理函数
3、一直不产生信号时,signal函数不会触发
4、ctrl + c时OS会向进程发送2号信号,执行2号信号SIGINT的默认动作,终止进程:
void(*handler)(int)
:是一个指向接受整型参数并返回void
的函数指针(我们自定义的信号处理函数也会被设置成这样子)
查看信号默认行为指令:man 7 signal
- Term:终止当前进程
- Core:终止当前进程并且Core Dump文件(进程被信号所杀时终止信号只占7比特位,其中第八个比特位就是为了判断否要生成core dump文件的标志位,使用ulimit + 相关选项可以查看core dump文件)
- Ign:忽略该信号
- Stop:停止当前进程
- Cont:继续执行先前停止的进程
信号产生方式
1、通过kill指令向指定进程发送指定信号
2、键盘的特殊按键组合可以产生信号,ctl+c == SIGINT(中断进程) ,ctl+z == SIGTSTP(暂停进程), ctl+\ == SIGQUIT(退出进程并生成用于调式的Core dump文件,该文件用于帮助我们进行debug)
3、系统调用接口kill可以向指定的进程发送指的信号,C语言库函数raise可以向当前进程发送信号,C语言的库函数abort可以
kill系统调用接口
函数原型:int kill(pid_t pid, int sig);
包含头文件:<signal.h> 和 <sys/types.h>
参数:指定进程pid,要发送的信号
返回值:发送成功返回0,发送失败返回-1
功能:向指定进程发送指定信号
process.cc文件:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
void hander(int sig)
{
std::cout << "sig of " << sig << " is already get " << std::endl;
}
int main()
{
signal(2, hander);
while (true)
{
std::cout << "pid = " << getpid() << " is wait" << std::endl;
sleep(1);
}
}
testsig.cc文件:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
// 想要以./mykill 2 进程pid 的形式运行程序
int main(int argc, char *argv[])
{
if (argc != 3) // 命令行参数不为3就报错返回
{
std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
return 1;
}
pid_t pid = std::stoi(argv[2]); // 从命令行字符串数组中获取进程pid
int signum = std::stoi(argv[1]); // 从命令行字符串数组中获取信号编号
kill(pid, signum);
return 0;
}
Makefile文件:
.PHONY:all
all:mykill myprocess
mykill:testsig.cc
g++ -o $@ $^ -std=c++11
myprocess:process.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mykill myprocess
raise函数
函数原型:int raise(int sig);
包含头文件:<signal.h>
参数:要发送的信号的编号(数字和宏都可以)
返回值:发送成功返回0,发送失败返回-1
功能:向当前进程发送信号
abort函数
函数原型:void abort(void);
包含头文件:<stdlib.h>
参数:没有参数
返回值:没有返回值
功能:调用时OS会向当前进程发送进程异常终止信号SIGABRT,引发程序的异常终止
alarm函数
函数原型:unsigned int alarm(unsigned int seconds);
包含头文件:<unistd.h>
参数:表示定时器的秒数,定时器计时到达指定的秒数后,会发送
SIGALRM
信号给调用进程返回值:表示上一个设置的定时器剩余的秒数。如果之前没有设置定时器,则返回0
功能:用于设置一个定时器,即在指定的秒数后向调用进程发送
SIGALRM
信号注意事项:
alarm
函数会设置一个全局的定时器,如果在之前已经调用过alarm
函数,再次调用会取消之前的定时器,并设置新的定时器。因此,需要注意在何时调用alarm
函数以避免意外的定时器重置
注意事项
1、普通信号中只有9号信号不允许自定义捕获(为了避免用户恶意屏蔽所有信号导致进程无法被杀死)
2、CPU通过溢出标志位来得知运算是正常的还是异常的
31、2:30处
3、程序崩溃是因为发生了非法访问或者操作、OS向进程发送了信号SIGSEGV,崩溃后的退出是因为该信号的默认行为就是进程退出,当然也可以不退出进行信号捕获,但是这样就会使得cpu寄存器中存放的进程的代码和溢出标志位等信息重新被放至CPU上然后一直循环这一过程,相反直接退出程序可以直接释放这些数据(主要是为了释放溢出标志信息及其它异常信息防止重复出错)
~over~