Linux的信号量 && PV操作 && 加解锁 && 信号的产生

目录

补充内容

信号量

信号量的函数

semget函数(获取标识符)

semctl函数(获取结构体信息)

semop函数(执行操作)

信号

signal函数

信号产生方式

kill系统调用接口

process.cc文件: 

testsig.cc文件:

Makefile文件:

raise函数

abort函数

alarm函数

注意事项


补充内容

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即对整个信号量集进行操作),指定要执行的操作(比如GETVALSETVAL等),可选参数(根据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_IGNhandler则将信号处理方式设置为忽略该信号,传递SIG_DFLhandler则将信号处理方式设置为默认操作

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~

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值