C++Linux高级编程 -- 应用开发重点

文章目录

  • centos 换源
  • 安装c++编译器并且更新
  • 编译:单文件以及多文件
    • 单文件
    • 多个文件编译
    • 编译选项
  • 静态库与动态库
    • 静态库
    • 动态库
  • makefile
  • main函数的参数
  • gdb调试
    • 基础
    • 调试core文件
    • 调试正在运行中的程序
  • Linux时间操作
    • time_t别名
    • time库函数
    • tm结构体
    • localtime()库函数
    • mktime库函数
    • gettimeofdat()库函数
    • 程序睡眠
  • Linux文件操作
    • 简单的目录操作
      • 获取当前工作目录
      • 切换工作目录
      • 创建目录
      • 删除目录
    • 获取存取权限access
    • stat()库函数
      • stat结构体
      • stat库函数
    • 修改目录或文件的时间utime
    • 重命名rename
    • 删除remove
    • 获取目录中文件的列表
  • Linux系统错误
    • strerror库函数
    • perror()库函数
    • 注意事项
  • Linux的信号
    • 基本概念
    • 信号的类型
    • 信号的处理
    • 信号有什么用
    • 发送信号
  • 进程终止
    • 进程终止的状态
    • 资源释放问题
    • 进程终止函数
  • 调用可执行程序
    • system函数
    • exec函数族
  • 创建进程
    • Linux的0、1和2号进程
    • 进程标识
    • fork函数
    • fork的用法
    • 共享文件
    • vfork()
  • 僵尸进程
    • 僵尸进程危害
    • 避免僵尸进程
  • 多进程与信号
  • 共享内存
    • 概念
    • shmget函数(创建/获取)
    • shmat函数(连接)
    • shmdt函数(分离)
    • shmctl函数(删除)
  • 循环队列、信号量、生产/消费者模型的源代码
    • 循环队列and普通使用
    • 循环队列and共享内存
    • 信号量and共享内存
    • 生产消费者模型(重难点)
  • 线程
  • 后续补充

centos 换源

1、确保更换之前确保自己安装wget,若是没安装wget直接安装即可

yum list wget

yum -y install wget

2、首先备份

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

3、下载阿里云的yum源到/etc/yum.repos.d/

wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

4、清除缓存

yum clean all

5、更新本地YUM缓存

yum makecache

安装c++编译器并且更新

安装

yum -y install gcc*

更新

1、安装CentOS的第三方软件源,如devtoolset:
sudo yum install -y centos-release-scl安装devtoolset软件集,其中包含更新的GCC版本。例如,要安装devtoolset-9
sudo yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils3、启用新版本的GCC
scl enable devtoolset-9 bash4、永久更改GCC版本,
echo "source /opt/rh/devtoolset-9/enable" >> ~/.bash_profile
source ~/.bash_profile

编译:单文件以及多文件

单文件

有文件 demo.cpp
在这里插入图片描述

编译
在这里插入图片描述

执行

在这里插入图片描述

多个文件编译

在这里插入图片描述

在这里插入图片描述

编译选项

g++ 选项 源代码文件1 ...

常用选项:

-o : 指定输出文件的名,这个名称不能和源文件同名。默认为a.out
-g :对源代码进行调试
-On:在编译、链接中进行优化处理,生成的可执行程序效率更高
-c:只编译,不链接成为可执行文件,通常用于把源文件编译成静态库或动态库
-std=c++11 支持C++11标准
-I:(大写i)指定头文件所在目录
-L:指定链接库所在目录
-l:指定链接库名

优化选项
在这里插入图片描述
如果选择了优化选项:编译时间更长、目标程序不可调试、有效果,但是不可能显著提升程序的性能

静态库与动态库

实际开发的时候,通常把函数和类份文件编写,称之为库。

在其他的程序中,可以使用库中的函数和类。

一般来说,通用的函数和类不提供源代码文件(安全性、机密性),而是编译成二进制文件(分为静态库和动态库)。

静态库

1、制作静态库
g++ -c -o lib 库名.a 源代码文件清单

2、使用静态库
不规范的做法:g++ 选项 源代码文件名清单 静态库名

规划做法:g++ 选项 源代码文件名清单 -L库名所在目录 -l库名

3、静态库的概念

程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接
如果多个程序中用到了同一静态库中的函数或类 ,就存在多份拷贝

4、静态库特点

  • 静态库的链接是在编译时期完成的,执行的时候代码加载速度快
  • 目标程序的可执行文件比较大,浪费空间
  • 程序的更新和发布不方便。如果某一个静态库更新了,所有使用它的程序都需要重新编译

实操:
在这里插入图片描述
编译
在这里插入图片描述
制作库
在这里插入图片描述
使用库
在这里插入图片描述

动态库

1、制作动态库
g++ -fPIC -shared -o lib 库名.so 源代码文件清单

2、使用动态库
不规范的做法:g++ 选项 源代码文件名清单 动态库名

规划做法:g++ 选项 源代码文件名清单 -L库名所在目录 -l库名
运行可执行程序的时候需要提前设置LD_LIBRARY_PATH环境变量

3、动态库的概念

程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入
如果多个程序中用到了同一静态库中的函数或类 ,那么在内存中只有一份,避免了空间的浪费

4、动态库特点

  • 程序运行在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存
  • 可以实现进程之间的代码共享,因此动态库也称为共享库
  • 程序升级变得简单,不需要重新编译程序,只需要更新动态库就行了

不规范的使用
在这里插入图片描述
在这里插入图片描述

将动态库目录加入LD_LIBRARY_PATH环境变量

在这里插入图片描述
在这里插入图片描述

makefile

make是一个强大的实用工具,用于管理的编译和链接。make需要一个编译规则文件makefile,可实现自动化编译

增量编译

# 变量
O=book.cpp 
# 指定编译的目标文件
all:book
# 编译book需要依赖book.cpp
book:book.cppg++ -o book book.cpp $(O)
# 用于清理book 使用make clean 触发
clean:rm book

main函数的参数

main函数有三个参数:arga、argv、envp

int main(int argc,char*argv[],char*envp[]){}
  • argc:程序参数的个数
  • argv:存放每个参数的值
  • envp:环境变量,用的少 ,数组最后一个是0
#include<iostream>using namespace std;
int main(int argc,char*argv[]){cout<<"一共"<<argc<<"个参数"<<endl;for(int i = 0;i < argc;++i){cout<<"第"<<i+1<<"个参数是"<<argv[i]<<endl;}return 0;
};

在这里插入图片描述

#include<iostream>using namespace std;
int main(int argc,char*argv[],char*envp[]){for(int i = 0;envp[i]!=0 ;++i){cout<<envp[i]<<endl;}setenv("AA","aaa",0);cout<<"AA="<<getenv("AA")<<endl;return 0;
};

在这里插入图片描述

gdb调试

基础

加上-g选项而且不能-O优化,才可以调试

gdb 文件

设置参数

命令简写说明
set args设置程序运行的参数
breakb设置断点,b 20表示在20行设置断点,可以设置多个断点
runr开始运行程序,遇到断点才停止
nextn执行当前行语句,如果该语句为函数调用,不会进入函数内部
steps执行当前行语句,如果该语句为函数调用,会进入函数内部。只是进入有源码的函数
printp显示变量或者表达式的值,也可以修改变量的值
continuec继续运行程序,遇到下一个断点停止,没有断点一直执行
set var设置变量的值, set var num=100
quitq退出gdb

调试core文件

如果程序在运行的过程中发生了内存泄漏,会被内核强行终止,提示“段错误(吐核)”,内存的状态将保存在 core 文件中,方便程序员进一步分析。

#include<iostream>using namespace std;
int main(int argc,char*argv[]){char*ptr = nullptr;*ptr=3;return 0;
};

在这里插入图片描述

Linux缺省不会生成core文件,需要修改系统参数

调式core文件的步骤如下

  1. 使用ulimit-a查看当前用户的资源限制参数
  2. ulimit -c unlimitedcore file size改为unlimited
  3. 运行程序,产生core文件
  4. 使用gdb main core文件名进行调试core文件
  5. bt查看函数调用栈

在这里插入图片描述
在这里插入图片描述

bt查看函数调用栈
在这里插入图片描述

调试正在运行中的程序

#include<iostream>
#include <unistd.h>using namespace std;void fun(){for(int i = 0;i < 10000;++i){sleep(1);cout<<"i="<<i<<endl;}
}
int main(int argc,char*argv[]){fun();return 0;
};

需要知道进程id
此时程序已经暂停
在这里插入图片描述

Linux时间操作

1970年1月1日组为UNIX的纪元时间,使用一个整数存放

time_t别名

time_t就是long类型的别名,在time.h文件中定义

typedef long time_t

time库函数

获取当前操作系统的时间

time.h

time_t time(time_t*tloc);//两种调用方法
time_t now = time(0);time_t now;
time(&now);

tm结构体

time_t是一个长整数,不符合人类的使用习惯,需要转换成tm 结构体,tm 结构体在<time.h>中

在这里插入图片描述

localtime()库函数

localtime()函数用于把 time_t表示的时间转换为 tm 结构体表示的时间。
localtime()函数不是线程安全的,localtime_r()是线程安全的。

time.h

在这里插入图片描述
年要+1900
月份要+1

mktime库函数

在这里插入图片描述

gettimeofdat()库函数

用于获取 1970 年1月1日到现在的秒和当前秒中已逝去的微秒数,可以用于程序的计时。

sys/time.h

在这里插入图片描述

程序睡眠

如果需要把程序挂起一段时间,可以使用sleep()和 usleep()两个库函数

unistd.h

sleep单位是秒
usleep单位是微秒
在这里插入图片描述

Linux文件操作

简单的目录操作

获取当前工作目录

头文件unistd.h

char *getcwd(char *buf, size_t size); 
char *get_current_dir_name(void);void test(){char path[256];//linux系统目录最大长度为255getcwd(path,256); std::cout<<"path="<<path<<std::endl;char *path0=get_current_dir_name();//malloc分配内存 是对空间开辟的std::cout<<"path0="<<path0<<std::endl;free(path0);//注意释放内存
}

切换工作目录

包含头文件:<unistd.h>
int chdir(const char *path);
返回值:0-成功;其它-失败(目录不存在或没有权限

创建目录

头文件sys/stat.h

int mkdir(const char *pathname, mode_t mode);
/*
pathname-目录名。
mode-访问权限,如0755,不要省略前置的0。
返回值:0-成功;其它-失败(上级目录不存在或没有权限)。 /tmp/aaa  /tmp/aaa/bbb 
*/

删除目录

包含头文件: unistd.h

int rmdir(const char *path);
/*
path-目录名。
返回值:0-成功;其它-失败(目录不存在或没有权限)。
*/

获取存取权限access

access()用于判断当前用户对目录或文件的存取权限

含头文件:#include <unistd.h>

函数声明:
int access(const char *pathname, int mode);

参数说明:
pathname 目录或文件名。
mode 需要判断的存取权限。

在头文件<unistd.h>中的预定义如下:

#define R_OK 	4 	// 判断是否有读权限。
#define W_OK	2 	// 判断是否有写权限。
#define X_OK 	1 	// 判断是否有执行权限。
#define F_OK 	0  	// 判断是否存在。

当pathname满足mode权限返回0,不满足返回-1,errno被设置。

在实际开发中,access()函数主要用于判断目录或文件是否存在

void fun(){int flag = access("/opt/tmp/AA",F_OK);std::cout<<errno<<"."<<strerror(errno)<<std::endl;std::cout<<flag<<std::endl;
}

stat()库函数

stat结构体

struct stat结构体用于存放目录或文件的详细信息,如下:

struct stat
{dev_t st_dev;   	// 文件的设备编号。ino_t st_ino;   		// 文件的i-node。mode_t st_mode; 	// 文件的类型和存取的权限。nlink_t st_nlink;   	// 连到该文件的硬连接数目,刚建立的文件值为1。uid_t st_uid;   		// 文件所有者的用户识别码。gid_t st_gid;   		// 文件所有者的组识别码。dev_t st_rdev;  	// 若此文件为设备文件,则为其设备编号。off_t st_size;  		// 文件的大小,以字节计算。size_t st_blksize;	// I/O 文件系统的I/O 缓冲区大小。size_t st_blocks;  	// 占用文件区块的个数。time_t st_atime;  	// 文件最近一次被存取或被执行的时间,// 在用mknod、 utime、read、write 与tructate 时改变。time_t st_mtime;  	// 文件最后一次被修改的时间,// 在用mknod、 utime 和write 时才会改变。time_t st_ctime;  	// 最近一次被更改的时间,在文件所有者、组、 权限被更改时更新。
};

struct stat结构体的成员变量比较多,重点关注st_mode、st_size和st_mtime成员

注意:st_mtime是一个整数表示的时间,需要程序员自己写代码转换格式。

st_mode成员的取值很多,用以下两个宏来判断:
S_ISREG(st_mode) // 是否为普通文件,如果是,返回真。
S_ISDIR(st_mode) // 是否为目录,如果是,返回真。

stat库函数

#include <sys/stat.h>

函数声明:int stat(const char *path, struct stat *buf);
stat()函数获取path参数指定目录或文件的详细信息,保存到buf结构体中。
返回值:0-成功,-1-失败,errno被设置。

void fun(){struct stat st;if((stat("/opt/codes/file.cpp",&st))!=0){std::cout<<"错误:"<<strerror(errno)<<std::endl;return;}if(S_ISREG(st.st_mode))std::cout<<"是一个文件,创建时间是"<<st.st_mtime<<std::endl;else if(S_ISDIR(st.st_mode))std::cout<<"是一个目录,创建时间是"<<st.st_mtime<<std::endl;
}

修改目录或文件的时间utime

#include <sys/types.h>
#include <utime.h>
函数声明:
int utime(const char *filename, const struct utimbuf *times);
utime()函数用来修改参数filename的st_atime和st_mtime。如果参数times为空地址,则设置为当前时间。结构utimbuf 声明如下:

struct utimbuf
{time_t actime;time_t modtime;
};

返回值:0-成功,-1-失败,errno被设置。

重命名rename

rename()函数用于重命名目录或文件,相当于操作系统的mv命令。

包含头文件:
#include <stdio.h>
函数声明:
int rename(const char *oldpath, const char *newpath);
参数说明:
oldpath 原目录或文件名。
newpath 目标目录或文件名。
返回值:0-成功,-1-失败,errno被设置。

删除remove

remove()函数用于删除目录或文件,相当于操作系统的rm命令。
包含头文件:
#include <stdio.h>
函数声明:
int remove(const char *pathname);
参数说明:
pathname 待删除的目录或文件名。
返回值:0-成功,-1-失败,errno被设置。

获取目录中文件的列表

文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表。

dirent.h

步骤一:用opendir()函数打开目录。
DIR *opendir(const char *pathname);
成功-返回目录的地址,失败-返回空地址。

步骤二:用readdir()函数循环的读取目录。
struct dirent *readdir(DIR *dirp);
成功-返回struct dirent结构体的地址,失败-返回空地址。

步骤三:用closedir()关闭目录。
int closedir(DIR *dirp);

目录指针:
DIR *目录指针变量名;

每次调用readdir(),函数返回struct dirent的地址,存放了本次读取到的内容。

struct dirent
{long d_ino;                    			// inode number 索引节点号。off_t d_off;                   			// offset to this dirent 在目录文件中的偏移。unsigned short d_reclen;     		// length of this d_name 文件名长度。unsigned char d_type;         		// the type of d_name 文件类型。char d_name [NAME_MAX+1];    // file name文件名,最长255字符。
};

重点关注结构体的d_name和d_type成员。
d_name-文件名或目录名。
d_type-文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-子目录(A directory),其它的暂时不关心。注意,d_name的数据类型是字符,不可直接显示。


void test(char *dir){DIR *d;if((d=opendir(dir))==nullptr)return;struct dirent*stdinfo=nullptr;while(true){if((stdinfo=readdir(d))==nullptr)break;std::cout<<"文件名="<<stdinfo->d_name<<",文件类型="<<int(stdinfo->d_type)<<std::endl;}closedir(d);
}

Linux系统错误

在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实,还有一个整型的全局变量errno,存放了函数调用过程中产生的错误代码。
如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。
errno在<errno.h>中声明。
配合 strerror()perror()两个库函数,可以查看出错的详细信息。

strerror库函数

string.h,用于获取错误代码对应的详细信息

char *strerror(int errnum);                       	       // 非线程安全。
int strerror_r(int errnum, char *buf, size_t buflen);		// 线程安全。

gcc8.3.1一共有133个错误代码。

    for(int i = 0;i < 200;++i){std::cout<<strerror(i)<<std::endl;}int iret = mkdir("/opt/tmp",0755);std::cout<<"iret="<<iret<<std::endl;std::cout<<errno<<"."<<strerror(errno)<<std::endl;

perror()库函数

perror() 在<stdio.h>中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大。(对调试程序略有帮助

void perror(const char *s);

    int iret = mkdir("/opt/tmp",0755);std::cout<<"iret="<<iret<<std::endl;std::cout<<errno<<"."<<strerror(errno)<<std::endl;perror("最近的信息");
}

注意事项

1)调用库函数失败不一定会设置errno
并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(一般来说,不属于系统调用的函数不会设置errno,属于系统调用的函数才会设置errno)。

什么是系统调用?百度“库函数和系统调用的区别”。

2)errno不能作为调用库函数失败的标志
errno的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动的置为 0。

在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。

    int iret = mkdir("/opt/tmp",0755);std::cout<<"iret="<<iret<<std::endl;std::cout<<errno<<"."<<strerror(errno)<<std::endl;iret = mkdir("/opt/tmp/aaa",0755);std::cout<<"iret="<<iret<<std::endl;std::cout<<errno<<"."<<strerror(errno)<<std::endl;perror("最近的信息");

上面的代码 第二次创建已经成功了,但是errno还是错误代码没有回归到0(成功代码)

在这里插入图片描述

Linux的信号

基本概念

信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。

信号产生的原因有很多。在shell中可以使用kill或者killall发送信号

kill -信号类型 进程编号
killall -信号类型 进程名

信号的类型

Linux有64种信号:
列出比较重要的:

信号名信号值默认处理动作发出信号的原因
SIGINT2A键盘中断CTRL+C
SIGKILL9AEF采用 kill -9 进程编号 强制杀死程序
SIGSEGV11CEF无效的内存引用(数组越界、操作空指针和野指针等)
SIGALRM14A由闹钟alarm()函数发出的信号
SIGTERM15A采用“采用“kill 进程编号”或“killall 程序名”通知程序”或“killall 程序名”通知程序
SIGCHLD17B子进程结束信号

其余的不是特别重要。

  • A 缺省的动作是终止进程
  • B 缺省的动作是忽略此信号,将该信号丢弃,不做处理。
  • C 缺省的动作是终止进程并进行内核映像转储(core dump)。
  • D 缺省的动作是停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。
  • E 信号不能被捕获。
  • F 信号不能被忽略。

信号的处理

进程对信号处理的方法有三种

  • 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程
  • 设置信号的处理函数,收到信号后,由该函数来处理
  • 忽略某个信号,对该信号不做任何处理,就像未发生过一样

默认、自定义、忽略

设置程序对信号处理方式的函数:signal()

函数声明:sighandler_t signal(int signum, sighandler_t handler);

  • 参数signum 表示信号的编号(信号的值)。
  • 参数handler表示信号的处理方式,有三种情况:
    1)SIG_DFL:恢复参数signum信号的处理方法为默认行为。
    2)一个自定义的处理信号的函数,函数的形参是信号的编号。
    3)SIG_IGN:忽略参数signum所指的信号。
#include<iostream>
#include<unistd.h>
#include<signal.h>void handle(int signNum){std::cout<<"收到信号:"<<signNum<<std::endl;exit(0);//退出进程
}
int main(int argc,char*argv[]){signal(15,handle);while(true){std::cout<<"运行中..."<<std::endl;sleep(1);}return 0;}

在这里插入图片描述

在这里插入图片描述

每五秒处理一下handle函数

#include<iostream>
#include<unistd.h>
#include<signal.h>void handle(int signNum){std::cout<<"收到信号:"<<signNum<<std::endl;alarm(5);
}
int main(int argc,char*argv[]){signal(14,handle);alarm(5);while(true){std::cout<<"运行中..."<<std::endl;sleep(1);}return 0;
}

第一个执行handle函数之后会把信号15设置为默认(终止进程)

#include<iostream>
#include<unistd.h>
#include<signal.h>void handle(int signNum){std::cout<<"收到信号:"<<signNum<<std::endl;signal(15,SIG_DFL);
}
int main(int argc,char*argv[]){signal(15,handle);while(true){std::cout<<"运行中..."<<std::endl;sleep(1);}return 0;
}

信号有什么用

  • 服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为进程被杀的时候,是突然死亡,没有安排善后工作。我们可以向服务程序发送一个信号,服务程序收到信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。

  • 如果向服务程序发送0的信号,可以检测程序是否存活

善后工作

#include<iostream>
#include<unistd.h>
#include<signal.h>void EXIT(int signNum){std::cout<<"保存资源...\n释放资源...\n程序退出\n";// 进程退出。exit(0);
}
int main(int argc,char*argv[]){// 忽略全部的信号,防止程序被信号异常中止for(int i = 1;i <= 64;++i)signal(i,SIG_IGN);// 如果收到2和15的信号(Ctrl+c和kill、killall),本程序将主动退出signal(2,EXIT);signal(15,EXIT);while(true){std::cout<<"工作中...\n";sleep(1);}return 0;
}

服务程序正在运行:无反应
服务程序没运行:如下错误
在这里插入图片描述

发送信号

Linux操作系统提供了killkillall命令向进程发送信号,在程序中,可以用kill()函数向其它进程发送信号。

函数声明:
int kill(pid_t pid, int sig);

kill()函数将参数sig指定的信号给参数pid 指定的进程。

参数pid 有几种情况:
1)pid>0 将信号传给**进程号为pid **的进程。

2)pid=0 将信号传给和当前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。

3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。

sig:准备发送的信号代码
假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在运行。

返回值说明: 成功执行时,返回0;失败返回-1,errno被设置。

进程终止

有8种方式可以终止紧张,

其中5种为正常终止:

  • main()函数用return返回
  • 任意函数中exit()函数
  • 在任意函数中调用_exit()或者_Exit()函数
  • 最后一个线程从其启动例程(线程主函数)用return返回;
  • 在最后一个线程中调用pthread_exit()返回;

异常终止:

  • abort()函数中止
  • 接受一个信号
  • 最后一个线程对取消请求做出响应

进程终止的状态

main()函数中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0

shell中查看进程终止状态echo $?

正常终止进程的3个函数(exit()和_Exit()是由ISO C说明的,_exit()是由POSIX说明的)。
void exit(int status);
void _exit(int status);
void _Exit(int status);
status也是进程终止的状态

如果进程被异常终止,终止状态为非0

终止状态应用:服务程序的调度、日志和监控

资源释放问题

  • return:调用局部对象的析构函数。main函数中的还会调用全局对象的析构函数
  • exit():不会调用局部对象的析构函数,会调用全局对象的析构函数,会执行清理工作
  • _exit() _Exit():直接退出,不会执行任何清理工作
#include<iostream>
#include<string.h>
using std::string;using std::cout;using std::endl;
class Test{
public:string name;Test(string name):name(name){cout<<"构造函数\n";}~Test(){cout<<name<<"析构函数\n";}
};Test t1("全局对象");
int main(){Test t2("局部对象");return 0;
}

在这里插入图片描述

#include<iostream>
#include<string.h>
using std::string;using std::cout;using std::endl;
class Test{
public:string name;Test(string name):name(name){cout<<"构造函数\n";}~Test(){cout<<name<<"析构函数\n";}
};Test t1("全局对象");
int main(){Test t2("局部对象");exit(0);
}

在这里插入图片描述

#include<iostream>
#include<string.h>
#include <unistd.h>
using std::string;using std::cout;using std::endl;
class Test{
public:string name;Test(string name):name(name){cout<<"构造函数\n";}~Test(){cout<<name<<"析构函数\n";}
};Test t1("全局对象");
int main(){Test t2("局部对象");_exit(0);
}

在这里插入图片描述

进程终止函数

进程可以用atexit()函数登记终止函数(最多32个),这些函数将由exit()自动调用
int atexit(void (*function)(void));
exit()调用终止函数的顺序与登记时相反。 (先入后出)

作用:进程退出前的收尾工作

#include<iostream>#include <unistd.h>
using std::cout;using std::endl;void fun1(){cout<<"fun1\n";
}
void fun2(){cout<<"fun2\n";
}
int main(){atexit(fun1);atexit(fun2);exit(0);
}

调用可执行程序

Linux提供了system()函数和exec()函数族,在C++程序中,可以执行其它的程序(二进制文件、操作系统命令或Shell脚本)

system函数

system()函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()函数就行了

函数的声明:
int system(const char * string);

system()函数的返回值比较麻烦:

1)如果执行的程序不存在,system()函数返回非0;
2)如果执行程序成功,并且被执行的程序终止状态是0,system()函数返回0;
3)如果执行程序成功,并且被执行的程序终止状态不是0,system()函数返回非0。

执行终止状态为0

#include<iostream>
#include <stdlib.h>
using std::cout;
using std::endl;int main(){int res = system("/bin/ls /opt");cout<<"res="<<res<<endl;perror("system");
}

在这里插入图片描述

有错误

#include<iostream>
#include <stdlib.h>
using std::cout;
using std::endl;int main(){int res = system("/bin/lsss /opt");cout<<"res="<<res<<endl;perror("system");
}

在这里插入图片描述

exec函数族

exec函数族提供了另一种在进程中调用程序(二进制文件或Shell脚本)的方法

exec函数族的声明如下:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,...,char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

注意:
1)如果执行程序失败则直接返回-1,失败原因存于errno中。
2)新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
3)如果执行成功则函数不会返回,当在主程序中成功调用exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行
4)在实际开发中,最常用的是execl()和ex ecv(),其它的极少使用。

#include <iostream>
#include <string.h>
#include <unistd.h>
using namespace std;int main(int argc,char *argv[])
{int ret=execl("/bin/ls","/bin/ls","-lt","/tmp",0);  // 最后一个参数0不能省略。//下面的是不会运行的//因为新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈cout << "ret=" << ret << endl;perror("execl");	/*char *args[10];args[0]="/bin/ls";args[1]="-lt";args[2]="/tmp";args[3]=0;     // 这行代码不能省略。int ret=execv("/bin/ls",args);cout << "ret=" << ret << endl;perror("execv");*/
}

创建进程

Linux的0、1和2号进程

整个Linux系统全部的进程是一个树形结构

0号进程(系统进程)是所有进程的祖先 ,它创建了1号和2号进程

1号进程(systemd)负责执行内核的初始化工作和进行系统配置

2号进程(kthreadd)负责所有内核线程的调度和管理

pstree可以查看进程树

pstree -p 进程编号

进程标识

每个进程都有一个非负整数表示的唯一的进程ID。

虽然是唯一的ID,但是进程ID可以复用。

当一个进程终止后,其进程ID就成为了复用的候选者

Linux采用延迟复用算法,让新创建进程的ID不同于最近终止的进程所使用的ID,这样防止了新进程被误认为是使用同一ID的某个已终止的进程

pid_t getpid(void); 获取当前进程的ID
pid_t getppid(void); 获取父进程的ID

fork函数

一个现有的进程可以调用fork()函数创建一个新进程

pid_t fork(void);

fork创建的是子进程,子进程是父进程的副本,父进程和子进程都从调用fork()之后的代码开始执行(fork之后的代码父进程和子进程都会执行)

fork()函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程的进程ID,通过返回值可以区分子进程和父进程

子进程获得了父进程数据空间、堆和栈的副本

注意

  • 子进程拥有的是副本,不是和父进程共享;
  • 但是子进程和主进程变量的地址是一样的,这里是虚拟地址而不是物理地址,系统做了处理只是看起来相同
  • 地址一样:如果存在引用,那么一开始就是确定了地址,子进程和主进程要访问引用必须是一样的地址
  • fork之后,主进程和子进程执行顺序是不确定的

地址相同,子进程修改变量不会影响父进程

#include<iostream>
#include<unistd.h>
#include<string.h>
using namespace std;
int main(){string name = "yb0os1";int num = 5;int&rnum = num;//引用指向地址不可变 所以子进程和父进程变量地址相同//子进程返回0 父进程返回子进程的pidpid_t pid = fork();if (pid>0){/* 父进程 */sleep(1);cout<<"父进程:"<<num<<"-"<<name<<endl;cout<<"父进程:"<<&num<<"-"<<&name<<endl;}else{/* 子进程 */name="yyy";num = 120;cout<<"子进程:"<<num<<"-"<<name<<endl;cout<<"子进程:"<<&num<<"-"<<&name<<endl;}return 0;
}

在这里插入图片描述

fork的用法

  • 父进程复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),让子进程处理些请求,而父进程则继续等待下一个连接请求
  • 进程要执行另一个程序。这种用法在Shell中很常见,子进程从fork()返回后立即调用exec

exec新进程取代旧进程,这样对父进程就没有影响
system就是这样原理(父进程创建一个子进程)

#include<iostream>
#include<unistd.h>using namespace std;
int main(){if(fork()>0){while(true){cout<<"父进程..."<<endl;sleep(1);}}else{sleep(5);execl("/bin/ls","/bin/ls","-lt","/opt",0);//0不可以省略cout<<"子进程结束..."<<endl;}return 0;
}

共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量,也就是说会写入两份,不会覆盖

如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。

#include <iostream>
#include <fstream>
#include <unistd.h>
using  namespace std;int main()
{ofstream fout;fout.open("/tmp/tmp.txt");  // 打开文件。fork();for (int ii=0;ii<10000000;ii++) // 向文件中写入一千万行数据。{fout << "进程" << getpid() << "西施" << ii << "极漂亮" << "\n";   }fout.close();    // 关闭文件。
}

在这里插入图片描述

vfork()

返回值和fork函数相同,但是两者语义不同

vfork函数创建一个进程,而该新进程的目的是exec一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。

另外一个区别:vfork()保证子进程先运行,在子进程调用**exec或exit()**之后父进程才恢复运行

僵尸进程

如果父进程比子进程先退出,那么子进程将被1号进程托管(这也是让程序在后台运行的方法)

#include<iostream>
#include<unistd.h>using namespace std;
int main(){if(fork()>0)return 0; //是主进程就退出//子进程后台运行for(int i = 0;i < 10;++i){sleep(1);cout<<"程序后台运行"<<endl;}return 0;
}
#include<iostream>
#include<unistd.h>using namespace std;
int main(){if(fork()>0){sleep(10);cout<<"主进程结束..."<<endl;}else{for(int i = 0;i < 100;++i){sleep(1);cout<<"子进程"<<endl;}}return 0;
}

在这里插入图片描述
在这里插入图片描述

如果子进程比父进程先退出,而且父进程没有处理子进程退出的信息,那么子进程将变成僵尸进程

僵尸进程

#include<iostream>
#include<unistd.h>using namespace std;
int main(){if(fork()==0)return 0; for(int i = 0;i < 100;++i){sleep(1);cout<<"父进程"<<endl;}return 0;
}

在这里插入图片描述

僵尸进程危害

内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。
父进程如果处理了子进程退出的信息,内核就会释放这个数据结构
父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用
系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程

避免僵尸进程

一、子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程用signal(SIGCHLD,SIG_IGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构

但是这样做父进程捕获不得子进程的退出信息

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main(){signal(SIGCHLD,SIG_IGN);if(fork()==0)return 0; for(int i = 0;i < 100;++i){sleep(1);cout<<"父进程"<<endl;}return 0;
}

在这里插入图片描述

二、父进程通过wait()/waitpid函数等待子进程结束,在子进程退出之前父进程将被阻塞

pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

返回的是子进程的编号

父进程会一直等待,wait之后的代码无法执行,直到子进程退出之后,wait之后的代码才可以正常执行

#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(){if(fork()>0){int sts;pid_t pid=wait(&sts);cout << "已终止的子进程编号是:" << pid<< endl;sleep(30);if(WIFEXITED(sts)){cout<<"子进程正常退出,退出状态是:"<<WEXITSTATUS(sts)<<endl;}else{cout<<"子进程是异常退出的,终止它的信号是:"<<WTERMSIG(sts)<<endl;}} else{pid_t pid = getpid();cout<<pid<<endl;sleep(10);return 0;}return 0;
}

在这里插入图片描述

三、如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()

这样可以实现父进程代码正常运行,并且可以处理子进程的退出

#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<signal.h>
using namespace std;
void handle(int sig){//这个形参是必须要有的int sts;pid_t pid = wait(&sts);cout<<"已终止的子进程pid:"<<pid<<endl;if(WIFEXITED(sts)){cout<<"子进程正常退出,退出的状态是:"<<WEXITSTATUS(sts)<<endl;}else{cout<<"子进程异常退出,终止的信号是:"<<WTERMSIG(sts)<<endl;}
}
int main(){signal(SIGCHLD,handle);if(fork()>0){while(true){sleep(1);cout<<"父进程在忙"<<endl;}}else{sleep(5);cout<<"我是子进程,pid:"<<getpid()<<",我父进程的pid:"<<getppid()<<endl;return 0;}return 0;
}

在这里插入图片描述

多进程与信号

在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出;如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出

子自己退,父管理子

#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void childEXIT(int sig);
void fatherEXIT(int sig);int main(){// 忽略全部的信号,不希望被打扰。for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);// 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程// 但请不要用 "kill -9 +进程号" 强行终止  SIGTERM 15  SIGINT 2signal(SIGTERM,fatherEXIT);signal(SIGINT,fatherEXIT);while(true){if(fork()>0){sleep(3);continue;}else{signal(SIGTERM,childEXIT);signal(SIGINT,SIG_IGN);//子进程后台运行 一般没办法触发ctrl+cwhile(true){cout<<"子进程"<<getpid()<<"正在运行"<<endl;sleep(3);}}}return 0;
}void childEXIT(int sig){// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断 一般子进程不会收到两次中断信号 但是加上也可以signal(SIGTERM,SIG_IGN);signal(SIGINT,SIG_IGN);cout << "子进程" << getpid() << "退出,sig=" << sig << endl;// 在这里增加释放资源的代码(只释放子进程的资源)exit(0);
}
void fatherEXIT(int sig){// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断 一般子进程不会收到两次中断信号 但是加上也可以signal(SIGTERM,SIG_IGN);signal(SIGINT,SIG_IGN);cout<<"父进程退出 sig="<<sig<<endl;kill(0,SIGTERM);//也会把中断信号发给自己 所以需要上面的代码// 在这里增加释放资源的代码(全局的资源)exit(0);
}

在这里插入图片描述
在这里插入图片描述

共享内存

在多线程中,多线程共享内存空间,如果多个线程想要访问同一块内存,定义全局变量就行

在多进程中,每个进程的地址空间是独立的,不共享的。如果多个进程想要访问同一块内存空间,不能使用全局变量,只能使用共享内存。

概念

共享内存(Shared Memory)允许多个进程(不要求进程之间有血缘关系)访问同一个内存空间,是多个进程之间共享和传递数据最高效的方式。

进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也会改变。

共享内存没有提供锁机制,也就是说,在某一个进程对共享内存进行读/写的时候,不会阻止其它进程对它的读/写。如果要对共享内存的读/写加锁,可以使用信号量。

Linux中提供了一组函数用于操作共享内存。

ipcs -m可以查看共享内存段
ipcrm -m 共享内存id 可以手工删除共享内存

shmget函数(创建/获取)

用于创建/获取共享内存

函数原型:
int shmget(key_t key, size_t size, int shmflg);

  • key:共享内存的键值,是一个无符号整数(typedef unsigned int key_t),一般采用十六进制,不同共享空间的key不相同
  • size:共享内存的大小,以字节为单位(sizeof计算也可以)
  • shmflg:共享内存的访问控制权限,与文件的权限一样,例如0666|IPC_CREAT。0666(八进制)表示全部用户对它可读写,IPC_CREAT表示如果不存在就创建
  • 返回值:成功返回共享内存的id,失败返回-1(系统内存不足、没有权限)
    注意:共享内存不可是STL容器,只能是C++内置数据类型
#include<iostream>
#include<sys/shm.h>
using namespace std;
struct Person{int age;char name[20];
};int main(){int shmid = shmget(0x0050,sizeof(Person),0666|IPC_CREAT);if(shmid==-1) {cout<<"shmget(0x5005) failed.\n"; return -1;}cout<<"创建成功:"<<shmid<<"\n";return 0;
}

在这里插入图片描述

shmat函数(连接)

该函数用于把共享内存连接到当前进程的地址空间

void *shmat(int shmid, const void *shmaddr, int shmflg);

  • shmid:由shmget返回的共享内存的标识
  • shmaddr:指定共享内存连接到当前进程中的地址位置,通常填0(让系统来选择共享内存的地址)
  • shmflg:标志位,通常填0
  • 调用成功时返回共享内存起始地址,失败返回(void*)-1

shmdt函数(分离)

该函数用于将共享内存从当前进程中分离,相当于shmat()函数的反操作

int shmdt(const void *shmaddr);

  • shmaddr:shmat函数返回的地址
  • 返回值:成功返回0,失败返回-1
#include<iostream>
#include<sys/shm.h>
using namespace std;
struct Person{//只允许是c++内置数据类型 不允许是STL容器int age;char name[20];
};int main(int argc,char *argv[]){if(argc!=3){cout<<"输入不符合 请按以下形式: ./demo name age"<<endl;return -1;}//创建or获取共享内存int shmid = shmget(0x0050,sizeof(Person),0666|IPC_CREAT);if(shmid==-1) {cout<<"shmget(0x5005) failed.\n"; return -1;}cout<<"shmid="<<shmid<<"\n";//连接到当前进程地址空间Person*ptr = (Person*)shmat(shmid,0,0);//shmat返回的是void* 所以需要强制转化一下cout<<"初始值:"<<ptr->name<<"-"<<ptr->age<<endl;strcpy(ptr->name,argv[1]);ptr->age = atoi(argv[2]);cout<<"赋值之后:"<<ptr->name<<"-"<<ptr->age<<endl;//从当前进程地址空间分离int isDel = shmdt(ptr);if(isDel!=-1)cout<<"将"<<ptr<<"从当前进程地址空间分离成功"<<endl;return 0;
}

在这里插入图片描述

shmctl函数(删除)

该函数用于操作共享内存,最常用的操作是删除共享内存
int shmctl(int shmid, int command, struct shmid_ds *buf);

  • shmid:shmget函数返回的共享内存的id
  • command:操作共享内存的指令,删除填 IPC_RMID
  • buf:操作共享内存的数据结构的地址,如果要删除共享内存填0
  • 调用成功时返回0,失败时返回-1

注意:用root创建的共享内存,不管创建的权限是什么,普通用户无法删除

#include<iostream>
#include<sys/shm.h>
using namespace std;
struct Person{//只允许是c++内置数据类型 不允许是STL容器int age;char name[20];
};int main(int argc,char *argv[]){if(argc!=3){cout<<"输入不符合 请按以下形式: ./demo name age"<<endl;return -1;}//创建or获取共享内存int shmid = shmget(0x0050,sizeof(Person),0666|IPC_CREAT);if(shmid==-1) {cout<<"shmget(0x5005) failed.\n"; return -1;}cout<<"shmid="<<shmid<<"\n";//连接到当前进程地址空间Person*ptr = (Person*)shmat(shmid,0,0);//shmat返回的是void* 所以需要强制转化一下if(ptr==(void*)-1){cout<<"连接失败"<<endl;return -1;}cout<<"初始值:"<<ptr->name<<"-"<<ptr->age<<endl;strcpy(ptr->name,argv[1]);ptr->age = atoi(argv[2]);cout<<"赋值之后:"<<ptr->name<<"-"<<ptr->age<<endl;//从当前进程地址空间分离int isDel = shmdt(ptr);if(isDel!=-1)cout<<"将"<<ptr<<"从当前进程地址空间分离成功"<<endl;//删除共享内存if(shmctl(shmid,IPC_RMID,0)==-1){cout<<"删除失败"<<endl;return -1;}return 0;
}

在这里插入图片描述
如果结构体使用了STL呢?
在这里插入图片描述
会出现段错误,为什么?
STL容器动态的分配堆区内存,堆区内存不属于共享内存

循环队列、信号量、生产/消费者模型的源代码

循环队列and普通使用

_public.h

#include<iostream>
#include<cstring>
template<typename T,int MaxLength>
class Squeue{
private:bool is_inited;//是否已经初始化int head;//头int tail;//尾T data[MaxLength];int length;//实际长度Squeue(const Squeue&)=delete;//禁用拷贝构造函数Squeue operator=(const Squeue&)=delete;//禁用拷贝赋值函数
public:Squeue(){init();}void init(){if(!is_inited){head=0;tail = MaxLength-1;is_inited = true;length=0;memset(data,0,sizeof(data));}}bool full(){return length == MaxLength;}bool push(T m){if(!full()){tail = (tail+1)%MaxLength;data[tail]=m;++length;return true;}std::cout<<"push failed!\n";return false;}int size(){return length;}bool empty(){return length==0;}T&front(){return data[head];}//如果加上判断是否为空,当为空的时候没有返回bool pop(){if(!empty()){head=(head+1)%MaxLength;--length;return true;}std::cout<<"pop failed!\n";return false;}void print(){for(int i=0;i<length;++i)std::cout<<"data["<<(head+i)%MaxLength<<"]="<<data[(head+i)%MaxLength]<<"\n";}
};

demo1.cpp

#include"_public.h"
using namespace std;int main(){Squeue<int,5>sq;sq.push(12);sq.push(4);sq.push(9);cout<<"sq.size()="<<sq.size()<<"\n";sq.print();sq.pop();sq.pop();sq.push(13);sq.push(42);sq.push(19);sq.push(123);sq.push(2);sq.push(10);cout<<"sq.size()="<<sq.size()<<"\n";sq.print();return 0;
}

在这里插入图片描述

循环队列and共享内存

_public.h

#include<iostream>
#include<cstring>
template<typename T,int MaxLength>
class Squeue{
private:bool is_inited;//是否已经初始化int head;//头int tail;//尾T data[MaxLength];int length;//实际长度Squeue(const Squeue&)=delete;//禁用拷贝构造函数Squeue operator=(const Squeue&)=delete;//禁用拷贝赋值函数
public:Squeue(){init();}void init(){if(!is_inited){head=0;tail = MaxLength-1;is_inited = true;length=0;memset(data,0,sizeof(data));}}bool full(){return length == MaxLength;}bool push(T m){if(!full()){tail = (tail+1)%MaxLength;data[tail]=m;++length;return true;}std::cout<<"push failed!\n";return false;}int size(){return length;}bool empty(){return length==0;}T&front(){return data[head];}bool pop(){if(!empty()){head=(head+1)%MaxLength;--length;return true;}std::cout<<"pop failed!\n";return false;}void print(){for(int i=0;i<length;++i)std::cout<<"data["<<(head+i)%MaxLength<<"]="<<data[(head+i)%MaxLength]<<"\n";}
};

demo2.cpp

#include"_public.h"
using namespace std;int main(){int shmid = shmget(0x0A5E,sizeof(Squeue<int,6>),0640|IPC_CREAT);if(shmid==-1){cout<<"shmget failed\n";return -1;}Squeue<int,6>*QQ = (Squeue<int,6>*)shmat(shmid,0,0);QQ->init();//不会自动调用构造函数 所以要initif(QQ==(void*)-1){cout<<"shmat failed!\n";return -1;}QQ->push(12);QQ->push(23);QQ->push(99);QQ->push(16);cout<<"队列的长度:"<<QQ->size()<<endl;QQ->print();cout<<"队头元素:"<<QQ->front()<<endl;QQ->pop();cout<<"队头元素:"<<QQ->front()<<endl;QQ->pop();QQ->push(61);QQ->push(78);QQ->push(8);QQ->push(6);QQ->push(112);QQ->push(65);cout<<"队列的长度:"<<QQ->size()<<endl;QQ->print();cout<<QQ->empty()<<endl;shmdt(QQ);return 0;
}

信号量and共享内存

信号量可以实现进程同步,本质是一个非负数的计数器,用于给共享内存资源建立一个标识,表示该共享资源被占用情况

两种操作

  • P操作(wait):将信号量值-1,如果信号量的值为0,将阻塞等待,直到信号量的值大于0
  • V操作(post):将信号量值+1,任何时候都不会阻塞

应用

  • 如果约定信号量的取值只有0或者1(0-资源不可用,1-资源可用),可以实现互斥锁
  • 如果约定信号量的取值表示可用资源的数量,可以实现生产/消费者模型

_public.h

#ifndef _PUBLIC
#define _PUBLIC#include<iostream>
#include<cstring>
#include <unistd.h>
#include<sys/shm.h>
#include<sys/sem.h>
#include<sys/types.h>
#include<sys/ipc.h>
template<typename T,int MaxLength>
class Squeue{
private:bool is_inited;//是否已经初始化int head;//头int tail;//尾T data[MaxLength];int length;//实际长度Squeue(const Squeue&)=delete;//禁用拷贝构造函数Squeue operator=(const Squeue&)=delete;//禁用拷贝赋值函数
public:Squeue(){init();}void init(){if(!is_inited){head=0;tail = MaxLength-1;is_inited = true;length=0;memset(data,0,sizeof(data));}}bool full(){return length == MaxLength;}bool push(T m){if(!full()){tail = (tail+1)%MaxLength;data[tail]=m;++length;return true;}std::cout<<"push failed!\n";return false;}int size(){return length;}bool empty(){return (length==0);}T& front(){return data[head];  }bool pop(){if(!empty()){head=(head+1)%MaxLength;--length;return true;}std::cout<<"pop failed!\n";return false;}void print(){for(int i=0;i<length;++i)std::cout<<"data["<<(head+i)%MaxLength<<"]="<<data[(head+i)%MaxLength]<<"\n";}
};class csemp{
private:union semun{int val;struct semid_ds *buf;unsigned short *arry;};int semid;//信号量id(描述符) 未初始化为-1  初始化之后为返回的id/*sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,在全部修改信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。sem_flg设置为0,操作系统不会把信号量恢复为初始值1、如果用于互斥锁,sem_flg设置为SEM_UNDO,也就是全部进程终止后,操作系统将锁设置为解锁状态2、如果用于生产/消费者模型,sem_flg设置为0, */short sem_flg;csemp(const csemp&)=delete;csemp& operator=(const csemp&)=delete;
public:csemp():semid(-1){}/*初始化:如果信号量已经存在,获取如果信号量不存在,创建,并且初始化为value用于互斥锁:value=1,sem_flg=SEM_UNDO用于生产/消费者模型:value=0,SEM_UNDO=0key:信号量的标识符(唯一)value:值sem_flg_y:*/bool init(key_t key,unsigned short value=1,short sem_flg_y=SEM_UNDO);bool wait(short value=-1);//P操作 -1 如果信号量的值是0,将阻塞等待,直到信号量的值大于0bool post(short value=1);//V操作  +1int getValue();//获取信号量的值,成功返回信号量 失败返回-1bool destroy();//销毁信号量~csemp()=default;
};#endif

_public.cpp

#include"_public.h"
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。default
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg_y){if(semid != -1)return false;//已经初始化sem_flg = sem_flg_y;// 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)// 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,// 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。// 信号量的初始化分三个步骤:// 1)获取信号量,如果成功,函数返回。// 2)如果失败,则创建信号量。// 3) 设置信号量的初始值。//获取信号量if((semid = semget(key,1,0640)) == -1){//信号量不存在if(errno == ENOENT){//创建信号量:用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取if((semid=semget(key,1,0640|IPC_CREAT|IPC_EXCL))==-1){//创建信号量失败,也就是已经存在信号量  再次获取信号量/*为什么上面已经判断信号量不存在这里还可能存在?多进程,存在多个进程同时进入获取的信号量不存在,但是只有一个创建,创建之后就存在了*/if(errno==EEXIST){if((semid=semget(key,1,0640))==-1){//获取失败perror("init 1 semget()");return false;} return true;}else{// 如果是其它错误,返回失败perror("init 2 semget()");return false;}}// 信号量创建成功后,还需要把它初始化成valueunion semun sem_union;sem_union.val = value;if(semctl(semid,0,SETVAL,sem_union)<0){perror("init semctl");return false;}}else{//其他错误(不是信号量不存在的错误)perror("init 3 semget");return false;}}return true;//获取成功直接返回true
}// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0
bool csemp::wait(short value){if(semid==-1)return false;//未初始化 不可操作struct sembuf sem_b={0,value,sem_flg};//和以下等价/*sem_b.sem_num = 0;//信号量编码 0代表第一个信号sem_b.sem_op = value; //p操作的value必须小于0sem_b.sem_flg = sem_flg; */if(semop(semid,&sem_b,1)==-1){//semop进行PV操作 是P还是V看v sembuf的valueperror("P semop()");return false;}return true;
}// 信号量的V操作(把信号量的值加value)。
bool csemp::post(short value){if(semid==-1)return false;struct sembuf sem_b={0,value,sem_flg};//和以下等价/*sem_b.sem_num = 0;//信号量编码 0代表第一个信号sem_b.sem_op = value; //p操作的value必须小于0sem_b.sem_flg = sem_flg; */if(semop(semid,&sem_b,1)==-1){perror("V semop()");return false;}return true;
}int csemp::getValue(){return semctl(semid,0,GETVAL);
}bool csemp::destroy(){if(semid==-1)return false;if(semctl(semid,0,IPC_RMID)==-1){perror("destroy semctl()");return false;}return true;
}

demo3.cpp

#include"_public.h"
using namespace std;
struct stgirl{int  id;        // 编号。char name[51];  // 姓名,注意,不能用string。
};int main(int argc,char*argv[]){if(argc!=3){cout<<"using ./demo id name\n";return -1;}int shmid = shmget(0x0500,sizeof(stgirl),0640|IPC_CREAT);if(shmid==-1){cout << "shmget(0x5005) failed.\n"; return -1; }stgirl*gl = (stgirl*)shmat(shmid,0,0);if(gl==(void*)-1){cout<<"shmat() failed\n";return -1;}csemp mutex;if(mutex.init(0x0500)==-1){cout<<"csemp init failed\n";return -1;}// 查看信号量  :ipcs -s    // 删除信号量  :ipcrm sem 信号量id// 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存idcout<<"lock...\n";mutex.wait();cout<<"old::id="<<gl->id<<",name="<<gl->name<<endl;gl->id = atoi(argv[1]);strcpy(gl->name,argv[2]);cout<<"new::id="<<gl->id<<",name="<<gl->name<<endl;sleep(10);mutex.post();cout<<"unlock...\n";shmdt(gl);// mutex.destroy();// if(shmctl(shmid,IPC_RMID,0)==-1){//     cout << "shmctl failed\n"; //     return -1; // }return 0;
}

生产消费者模型(重难点)

生产者

//生产者
#include"_public.h"
using namespace std;
using Elem = Person;
int main(){// 初始化共享内存int shmid = shmget(0x0555,sizeof(Squeue<Elem,5>),0640|IPC_CREAT);if(shmid==-1){cout<<"shmget failed!\n";return -1;}// 把共享内存连接到当前进程的地址空间Squeue<Elem,5>*QQ = (Squeue<Elem,5>*)shmat(shmid,0,0);if(QQ==(void*)-1){cout<<"shmat failed!\n";return -1;}QQ->init();// 初始化循环队列Elem e; // 创建一个数据元素csemp mutex;mutex.init(0x0501);// 用于给共享内存加锁csemp cond;cond.init(0x0502,0,0);// 信号量的值用于表示队列中数据元素的个数mutex.wait();//上锁e.age=18;e.sex='M';strcpy(e.name,"彬彬");QQ->push(e);e.age=28;e.sex='W';strcpy(e.name,"西施");QQ->push(e);e.age=24;e.sex='W';strcpy(e.name,"貂蝉");QQ->push(e);mutex.post();//解锁cond.post(3);//实参是3,表示生产了3个数据shmdt(QQ);// // 把共享内存从当前进程中分离return 0;
}

消费者

//消费者
#include"_public.h"
using namespace std;
using Elem = Person;
int main(){// 获取共享内存int shmid = shmget(0x0555,sizeof(Squeue<Elem,5>),0640|IPC_CREAT);if(shmid==-1){cout<<"shmget failed!\n";return -1;}// 把共享内存连接到当前进程的地址空间Squeue<Elem,5>*QQ = (Squeue<Elem,5>*)shmat(shmid,0,0);if(QQ==(void*)-1){cout<<"shmat failed!\n";return -1;}QQ->init();// 初始化循环队列Elem e; // 创建一个数据元素csemp mutex;mutex.init(0x0501);// 用于给共享内存加锁csemp cond;cond.init(0x0502,0,0);// 信号量的值用于表示队列中数据元素的个数while(true){mutex.wait();//加锁while(QQ->empty()){mutex.post();//解锁cond.wait();// 等待生产者的唤醒信号  QQ是空就是代表cond的信号量为0 为0P操作则等待mutex.wait();}// 数据出队e = QQ->front();QQ->pop();mutex.post(); // 解锁cout <<"name="<< e.name<<",age=" << e.age << ",sex=" << ((e.sex=='M')?"男":"女")<<endl;sleep(1);}shmdt(QQ);
}

_public.h

#ifndef _PUBLIC
#define _PUBLIC#include<iostream>
#include<cstring>
#include <unistd.h>
#include<sys/shm.h>
#include<sys/sem.h>
#include<sys/types.h>
#include<sys/ipc.h>
struct Person{int age;char sex;char name[51];
};
template<typename T,int MaxLength>
class Squeue{
private:bool is_inited;//是否已经初始化int head;//头int tail;//尾T data[MaxLength];int length;//实际长度Squeue(const Squeue&)=delete;//禁用拷贝构造函数Squeue operator=(const Squeue&)=delete;//禁用拷贝赋值函数
public:Squeue(){init();}void init(){if(!is_inited){head=0;tail = MaxLength-1;is_inited = true;length=0;memset(data,0,sizeof(data));}}bool full(){return length == MaxLength;}bool push(T m){if(!full()){tail = (tail+1)%MaxLength;data[tail]=m;++length;return true;}std::cout<<"push failed!\n";return false;}int size(){return length;}bool empty(){return (length==0);}T& front(){return data[head];  }bool pop(){if(!empty()){head=(head+1)%MaxLength;--length;return true;}std::cout<<"pop failed!\n";return false;}void print(){for(int i=0;i<length;++i)std::cout<<"data["<<(head+i)%MaxLength<<"]="<<data[(head+i)%MaxLength]<<"\n";}
};class csemp{
private:union semun{int val;struct semid_ds *buf;unsigned short *arry;};int semid;//信号量id(描述符) 未初始化为-1  初始化之后为返回的id/*sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,在全部修改信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。sem_flg设置为0,操作系统不会把信号量恢复为初始值1、如果用于互斥锁,sem_flg设置为SEM_UNDO,也就是全部进程终止后,操作系统将锁设置为解锁状态2、如果用于生产/消费者模型,sem_flg设置为0, */short sem_flg;csemp(const csemp&)=delete;csemp& operator=(const csemp&)=delete;
public:csemp():semid(-1){}/*初始化:如果信号量已经存在,获取如果信号量不存在,创建,并且初始化为value用于互斥锁:value=1,sem_flg=SEM_UNDO用于生产/消费者模型:value=0,SEM_UNDO=0key:信号量的标识符(唯一)value:值sem_flg_y:*/bool init(key_t key,unsigned short value=1,short sem_flg_y=SEM_UNDO);bool wait(short value=-1);//P操作 -1 如果信号量的值是0,将阻塞等待,直到信号量的值大于0bool post(short value=1);//V操作  +1int getValue();//获取信号量的值,成功返回信号量 失败返回-1bool destroy();//销毁信号量~csemp()=default;
};#endif

_public.cpp

#include"_public.h"
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。default
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg_y){if(semid != -1)return false;//已经初始化sem_flg = sem_flg_y;// 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)// 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,// 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。// 信号量的初始化分三个步骤:// 1)获取信号量,如果成功,函数返回。// 2)如果失败,则创建信号量。// 3) 设置信号量的初始值。//获取信号量if((semid = semget(key,1,0640)) == -1){//信号量不存在if(errno == ENOENT){//创建信号量:用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取if((semid=semget(key,1,0640|IPC_CREAT|IPC_EXCL))==-1){//创建信号量失败,也就是已经存在信号量  再次获取信号量/*为什么上面已经判断信号量不存在这里还可能存在?上面两个操作不是原子操作多进程,存在多个进程同时进入获取的信号量不存在,但是只有一个创建,创建之后就存在了*/if(errno==EEXIST){if((semid=semget(key,1,0640))==-1){//获取失败perror("init 1 semget()");return false;} return true;}else{// 如果是其它错误,返回失败perror("init 2 semget()");return false;}}// 信号量创建成功后,还需要把它初始化成valueunion semun sem_union;sem_union.val = value;if(semctl(semid,0,SETVAL,sem_union)<0){perror("init semctl");return false;}}else{//其他错误(不是信号量不存在的错误)perror("init 3 semget");return false;}}return true;//获取成功直接返回true
}// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0
bool csemp::wait(short value){if(semid==-1)return false;//未初始化 不可操作struct sembuf sem_b={0,value,sem_flg};//和以下等价/*sem_b.sem_num = 0;//信号量编码 0代表第一个信号sem_b.sem_op = value; //p操作的value必须小于0sem_b.sem_flg = sem_flg; */if(semop(semid,&sem_b,1)==-1){//semop进行PV操作 是P还是V看v sembuf的valueperror("P semop()");return false;}return true;
}// 信号量的V操作(把信号量的值加value)。
bool csemp::post(short value){if(semid==-1)return false;struct sembuf sem_b={0,value,sem_flg};//和以下等价/*sem_b.sem_num = 0;//信号量编码 0代表第一个信号sem_b.sem_op = value; //p操作的value必须小于0sem_b.sem_flg = sem_flg; */if(semop(semid,&sem_b,1)==-1){perror("V semop()");return false;}return true;
}int csemp::getValue(){return semctl(semid,0,GETVAL);
}bool csemp::destroy(){if(semid==-1)return false;if(semctl(semid,0,IPC_RMID)==-1){perror("destroy semctl()");return false;}return true;
}

线程

后续更新分p

后续补充

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/311378.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【零基础入门TypeScript】模块

目录 内部模块 内部模块语法&#xff08;旧&#xff09; 命名空间语法&#xff08;新&#xff09; 两种情况下生成的 JavaScript 是相同的 外部模块 选择模块加载器 定义外部模块 句法 例子 文件&#xff1a;IShape.js 文件&#xff1a;Circle.js 文件&#xff1a;…

Windows下使用PanguVip实现浮动IP

在某些高可用场景下&#xff0c;我们往往需要使用浮动IP来进行实际访问的切换&#xff0c;比如为了保证Web应用的高可用&#xff0c;当主节点宕机后&#xff0c;我们将浮动IP切换到备节点&#xff0c;那么备节点就继续可以提供服务&#xff0c;在linux下我们可以使用keepalived…

【鸿蒙开发】饿了么页面练习

0. 整体结构 整体划分3部分。店铺部分&#xff0c;购物车部分&#xff0c;金额统计部分。使用 Stack 把3部分堆叠 0.1 整体页面 Index.ets 修改 Index.ets &#xff0c;使用堆叠布局&#xff0c;并居底部对齐 import { ElShop } from ../components/ElShop import { ElShopp…

php:实现压缩文件上传、解压、文件更名、压缩包删除功能

效果图 1.上传文件 2.压缩包文件 3.itemno1文件 4.上传到系统路径\ItemNo 5.更名后的itemno1文件(命名&#xff1a;当天日期六位随机数) 代码 <form action"<?php echo htmlspecialchars($_SERVER[PHP_SELF], ENT_QUOTES, UTF-8); ?>" method"post…

ChatGPT基础(二) ChatGPT的使用和调优

文章目录 ChatGPT的特性采用关键词进行提问给ChatGPT指定身份提升问答质量的策略1.表述方式上的优化2.用"继续"输出长内容3.营造场景4.由浅入深&#xff0c;提升问题质量5.预设回答框架和风格 ChatGPT的特性 1.能够联系上下文进行回答 ChatGPT回答问题是有上下文的&…

Java --- 类与对象

上篇内容给大家带来了Java的语句与数组的相关内容&#xff0c;那么本期内容比较重要&#xff0c;需要读者们掌握Java面向对象编程的根本&#xff0c;通过这篇博客来让读者浅入理解Java类的一些基本操作。 目录 一.特点&#xff1a; 二.成员变量&#xff1a; 三.访问修饰符&a…

【JavaSE】搞定String类

前言 本篇会细致讲解String类的常见用法&#xff0c;让小伙伴们搞定String类~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 前言 常用的三种字符串构造 字符串长度length 字符串比较 比较 比较字符串的内容equals…

「2024」React 状态管理入门

概念 简单来说&#xff0c;状态指的是某一时刻应用中的数据或界面的呈现。这些数据可能包括用户填写表单的信息、应用内的用户偏好设置、应用的页面/路由状态、或者任何其他可能改变UI的信息。 状态管理是前端开发中处理用户界面(UI)状态的过程&#xff0c;在复杂应用中尤其重…

【DA-CLIP】图像退化类型检测功能演示代码

背景 在CLIP基础上微调而来&#xff0c;使用图像控制器编码生成退化类型embedding并在训练中对图像编码器进行控制。针对十种退化类型进行了训练。 解决CLIP模型在图像纹理等层面无法针对退化类型识别或识别率较低的问题。 训练数据集情况 GitHub有对应数据集连接 完整代码 项…

HBase的数据模型与架构

官方文档&#xff1a;Apache HBase – Apache HBase™ Homehttps://hbase.apache.org/ 一、HBase概述 1.概述 HBase的技术源自Google的BigTable论文&#xff0c;HBase建立在Hadoop之上&#xff0c;是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统&#xff0c;用于…

工作流引擎项目解析

API 编辑 在Camunda中&#xff0c;API的继承关系主要体现在各个服务接口之间。以下是Camunda中一些常见服务接口的继承关系&#xff1a; ProcessEngineServices 接口&#xff1a; RepositoryService&#xff1a; 负责管理流程定义和部署。 RuntimeService&#xff1a; 负责管…

2024年nodejs调用小红书最新关注(粉丝)follow接口,api接口分析2004-04-16

一、打开chrome按f12&#xff0c;点击右上角的“关注”按钮&#xff0c;抓包位置如下&#xff1a; (图1 follow接口) 二、follow接口分析 1、请求地址 https://edith.xiaohongshu.com/api/sns/web/v1/user/follow 2、请求方法: POST 3、请求头&#xff1a; :authority: edith…

最新AI创作系统ChatGPT网站源码AI绘画,GPTs,AI换脸支持,GPT联网提问、DALL-E3文生图

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧。已支持GPT…

25. 【Android教程】列表控件 ListView

在学习了 ScrollView 及 Adapter 两节内容之后&#xff0c;大家应该对 ListView 有了一些基本的了解&#xff0c;它是一个列表样式的 ViewGroup&#xff0c;将若干 item 按行排列。ListView 是一个很基本的控件也是 Android 中最重要的控件之一。它可以帮助我们完成多个 View 的…

Udio——革命性的AI音乐生成软件

Udio是一款革命性的AI音乐生成软件&#xff0c;由前谷歌DeepMind的顶尖AI研究人员和工程师共同创立&#xff0c;得到著名风险投资公司a16z的支持。它旨在为音乐爱好者和专业人士提供一个全新的音乐创作和分享平台。用户可以通过文本提示来生成音乐&#xff0c;支持广泛的音乐风…

水牛社:打造你的居家副业利器,让赚钱变得更简单

水牛社&#xff0c;这个已经陪伴我们走过九个春秋的综合性网络任务和项目资源整合平台&#xff0c;真的给我留下了深刻的印象。它不仅仅是一个资源的汇聚地&#xff0c;更是一个帮助无数人实现网络副业梦想的平台。作为一个资深用户&#xff0c;我深感其中的价值和魅力。 水牛…

“香港批准比特币、以太坊ETF”!华夏、博时、嘉实计划发行相关产品!美国ETF分析师泼冷水:别指望香港ETF会有很大流量!

周一(4月15日)&#xff0c;比特币短线迎来反弹行情&#xff0c;币价回升至66000美元上方。华夏基金、博时国际与嘉实投资3家中国头部基金宣布&#xff0c;旗下虚拟资产现货ETF获准在香港发行。 据了解&#xff0c;华夏基金&#xff08;香港&#xff09;现计划发行能够投资于现货…

两部电话机怎样能实现对讲?直接连接能互相通话吗?门卫门房传达室岗亭电话怎么搞?

目录 两部电话机能直接连接吗&#xff1f;用三通头分出来一条电话线两部电话机用一根电话线直接连接能互相通话吗&#xff1f; 什么电话机可以直接连接两部IP电话机&#xff08;网络电话机&#xff09;可以直接连接两部普通电话机之间通过一个电话交换机也可以连接跨区域的两部…

mfc 带有复选框的ListBox

mfc 带有复选框的 ListBox 效果&#xff1a; 添加 ListBox 控件 从工具箱拖拽 ListBox 控件到窗口上&#xff0c;并设置属性&#xff1a; 包含字符串&#xff1a;true所有者描述&#xff1a;Fixed 给ListBox添加控制变量 添加完后&#xff0c;将m_list_box的类型使用CC…

react使用npm i @reduxjs/toolkit react-redux

npm i reduxjs/toolkit react-redux 创建一个 store文件夹&#xff0c;里面创建index.js文件和子模块文件夹 index,js文件写入以下代码 import {configureStore} from reduxjs/toolkit // 导入子模块 import counterReducer from ./modules/one import two from ./modules/tw…