AWS LambdaでAWS利用費を毎朝Slackに通知する

f:id:ryskit:20200102190802p:plain

明けましておめでとうございます! 今年もよろしくお願いします!

今年は、「個人ブログで技術ネタ50記事書く」という目標も立てたので早速1本目を書きます!

今回作りたいのはAWS Lambdaを使ってAWS利用費を毎朝通知する仕組みです。

イメージとしては、今日が2019年12月25日なら、2019年12月1日から2019年12月24日までのAWS利用費を朝7:00にSlackに通知するというものです。

では、さっそくいってみましょう!

Slack Appを作る

まず、AWS利用費を通知するためにSlack Appを作成して通知先のエンドポイントを発行する必要があります。

以下のURLにアクセスします。

https://api.slack.com/apps

アクセスしたら、 Create New App をクリックします。

f:id:ryskit:20200102172328p:plain

クリックするとモーダルが表示されるので、名前Workspace を指定します。

Workspace は自身のものや会社のSlackのWordspaceを指定すれば良いと思います。

f:id:ryskit:20200102172701p:plain

Create App をしたら以下のように Basic Informationのページに遷移するので、 そこで表示されている、Incoming Webhook をクリックします。

f:id:ryskit:20200102173102p:plain

Incoming Webhook をクリックしたら、以下のページに遷移します。

Activate Incoming webhookをon にしてあげます。

f:id:ryskit:20200102174011p:plain

すると、以下のように表示内容が変わります。

まだ、WorkspaceにWebhookを追加してない状態なので、Add New Webhook to Workspace をクリックします。

f:id:ryskit:20200102174209p:plain

クリックしたら、以下のようにアクセス権限を与えて良いか確認されます。 どのチャンネルにAWS利用費を通知するか決めたら、そのチャンネルを選択して、許可 をクリックします。

f:id:ryskit:20200102173417p:plain

許可したら、Webhookが追加されます。

ここで発行されたWebhook URLが必要になってくるので、コピーしておくなどしておいてください。

f:id:ryskit:20200102174734p:plain

ひとまず、Slack Appの準備は完了です。

AWS Lambdaのコードを書く

次はSlackへ通知するための、AWS Lambdaを作っていきます。 サクッとやってしまいたいのでFramework等は使わず、GoでLambdaを書いてみたかったので言語はGoを使います。

コードはこんな感じ。

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/costexplorer"
    "net/http"
    "time"
)

const (
    SlackApi   string = "ここにSlack Appで発行したWebhook URLをコピペする"
    DateLayout string = "2006-01-02"
)

func NewCostExplorerClient() *costexplorer.CostExplorer {
    sess := session.Must(session.NewSessionWithOptions(session.Options{
        Config: aws.Config{
            Region: aws.String("ap-northeast-1"),
        },
        SharedConfigState: session.SharedConfigEnable,
    }))
    return costexplorer.New(sess, aws.NewConfig().WithRegion("ap-northeast-1"))
}

type CostInfo struct {
    Start  string `json:"start"`
    End    string `json:"end"`
    Amount string `json:"amount"`
}

type SlackMessage struct {
    Text string `json:"text"`
    Mrkdwn bool `json:"mrkdwn"`
}

type TimeStringHelper struct {
    Location *time.Location
    Now time.Time
}

func NewTimeStringHelper(locationName string) *TimeStringHelper {
    location, _ := time.LoadLocation(locationName)
    return &TimeStringHelper{
        Location: location,
        Now: time.Now().In(location),
    }
}

func (helper TimeStringHelper) GetBeginningOfLastMonth() string {
    return time.Date(helper.Now.Year(), helper.Now.Month() -1, 1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetBeginningOfMonth() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), 1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetYesterday() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), helper.Now.Day() -1, 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) GetToday() string {
    return time.Date(helper.Now.Year(), helper.Now.Month(), helper.Now.Day(), 0, 0, 0, 0, helper.Location).Format(DateLayout)
}

func (helper TimeStringHelper) IsTodayFirst() bool {
    return helper.Now.Day() == 1
}

func (helper TimeStringHelper) GetStartTimePeriod() string {
    if helper.IsTodayFirst() {
        return helper.GetBeginningOfLastMonth()
    } else {
        return helper.GetBeginningOfMonth()
    }
}


func GetCostInfo(helper *TimeStringHelper) *CostInfo {
    start := helper.GetStartTimePeriod()
    end := helper.GetToday()
    costExplorer := NewCostExplorerClient()
    output, err := costExplorer.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{
        Granularity: aws.String("MONTHLY"),
        Metrics: []*string{
            aws.String("AmortizedCost"),
        },
        TimePeriod: &costexplorer.DateInterval{
            Start: aws.String(start),
            End:   aws.String(end),
        },
    })
    if err != nil {
        panic(err)
    }
    total := output.ResultsByTime[0].Total["AmortizedCost"]
    amount := aws.StringValue(total.Amount)

    return &CostInfo{
        Start:  start,
        End:    end,
        Amount: amount,
    }
}

func makeSlackMessage(costInfo *CostInfo, helper *TimeStringHelper) SlackMessage {
    return SlackMessage{
        Text: fmt.Sprintf("*期間*: `%s ~ %s`\n*料金*: `$%s`",
            costInfo.Start,
            helper.GetYesterday(),
            costInfo.Amount),
        Mrkdwn: true}
}

func PostToSlack(message SlackMessage) {
    input, _ := json.Marshal(message)
    fmt.Println(string(input))
    http.Post(SlackApi, "application/json", bytes.NewBuffer(input))
}

type Response struct {
    Message []byte `json:"message"`
}

func BillingNotification(ctx context.Context) (Response, error) {
    helper := NewTimeStringHelper("Asia/Tokyo")
    fmt.Println(helper.Now.Format("2006/01/02 15:04:05"))
    costInfo := GetCostInfo(helper)
    message := makeSlackMessage(costInfo, helper)
    PostToSlack(message)
    json, _ := json.Marshal(message)
    return Response{Message: json}, nil
}

func main() {
    lambda.Start(BillingNotification)
}

Githubにもアップしてるので、試してみたい方はぜひ使ってみてください。

github.com

コードに関して全ては説明しないですが、ピックアップして説明します。

AWS利用費の取得には、Cost Explorer を使います。 以下の部分ですね。

GranularityにはMONTHLY を指定します。 TimePeriodは、利用料の取得期間です。 たとえば、Startに 2019-12-01 、 Endに 2019-12-31を指定すると、 2019-12-01 から 2019-12-30までのAWS利用料を取得できます。 ここが間違えやすところかもしれません。

参考: https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html#awscostmanagement-GetCostAndUsage-request-TimePeriod

output, err := costExplorer.GetCostAndUsage(&costexplorer.GetCostAndUsageInput{
    Granularity: aws.String("MONTHLY"),
    Metrics: []*string{
        aws.String("AmortizedCost"),
    },
    TimePeriod: &costexplorer.DateInterval{
        Start: aws.String(start),
        End:   aws.String(end),
    },
})

Slackへ通知する際のmessageのpayloadも決まっているので、 それに合わせてメッセージをPostする必要があります。

func makeSlackMessage(costInfo *CostInfo, helper *TimeStringHelper) SlackMessage {
    return SlackMessage{
        Text: fmt.Sprintf("*期間*: `%s ~ %s`\n*料金*: `$%s`",
            costInfo.Start,
            helper.GetYesterday(),
            costInfo.Amount),
        Mrkdwn: true}
}

参照: api.slack.com

定数のSlackApi にはSlack Appで発行したWebhookのURLを貼り付けておいてください。

これができたら、ビルドしてzip化します。

ビルドは以下のコマンドで実行してください。

オプションをつけ忘れるとLambdaが実行できないので気をつけましょう!

GOARCH=amd64 GOOS=linux go build

ビルドしたら実行ファイルが生成されるので、以下のコマンドでzip化します。

zip billing-notification.zip ./billing-notification

そうすると、billing-notification.zip というzipファイルが生成されます。 これはあとでAWSコンソールでLambda関数を作るときに使います。

AWSコンソールからLambda実行用IAMサービスロールを作成する

IAMサービスロールにアタッチするポリシーのJSONは以下のように書きます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ce:GetCostAndUsage"
            ],
            "Resource": "*"
        }
    ]
}

IAMサービスロールはLambdaを選択してください。

f:id:ryskit:20200102181522p:plain

あとはよしなにIAMサービスロールを作成します。

AWSコンソールからLambdaの関数を作成する

AWSコンソールからLambdaを開いて作成してきます。

関数名: billing-notification ランタイム: Go 1.x 実行ロール: 先程作成したサービスロールを指定するので、既存のロールを使用する を選択し、セレクトボックスから作成したロールを探して選択します。

f:id:ryskit:20200102182049p:plain

関数を作成できたら、LambdaのトリガーにCloudWatch Events を指定しておいてください。

そして、zip化したLambdaのコードをアップロードします。

ハンドラ名は billing-notification と入力し、保存します。

f:id:ryskit:20200102182941p:plain

もうこれでSlackに通知することができます!

試しにテストしてみましょう。

テストイベントの設定をします。 作成したことがあれば以下のように表示されますし、してなければ テストイベントの設定 と表示されているはず?なので、 テスト をクリックします。

f:id:ryskit:20200102183614p:plain

もし新規で作る場合は以下のようにモーダルが表示されるので、適当な名前をつけて保存してください。

f:id:ryskit:20200102183805p:plain

そして、もう一度テストをクリックすると、Slackに利用料が通知されるはず!!

f:id:ryskit:20200102183930p:plain

ほぼできた🎉🎉🎉

作業もあと少しです!

AWSコンソールからCloudWatch Eventsの設定

Slackに通知する仕組みは作れました。

あとは、毎朝7時にAWS Lambdaを実行するようにしておきたいですよね?

そのためにCloucWatch Eventsを利用します。

Lambdaを作成したときにトリガーにCloudWatch Eventsを指定したのはそのためです。

それでは、AWSコンソールからCloudWatchを開きます。

開いたら以下のように、ルールと書かれた箇所をクリックします。

f:id:ryskit:20200102184644p:plain

クリックしたら以下のようにルールの作成をクリックします。

f:id:ryskit:20200102184907p:plain

そうしたら、以下のように設定してみてください。

Cron式0 22 * * ? * と指定していますが、UTC表記なのでこのままで問題ありません。

設定できたら 設定の詳細 をクリックします。

f:id:ryskit:20200102184801p:plain

ステップ 2: ルールの詳細を設定する 画面では、状態を有効化してルールを作成してください。

これで、毎朝7時にLambdaが実行されてSlackに通知が来るはずです🎉🎉🎉

f:id:ryskit:20200102185242p:plain

最後に

いかがでしたでしょうか?

Lambdaを使えばサクッとAWS利用費を通知できるので、今どれぐらい使っていて請求が来るのか把握しやすくなりました!

もしこの記事が良ければ、Twitterなどでシェアしてくださいね!

読んでくださってありがとうございました!

※ アイキャッチには@tentenのGopher by tenntenn CC BY 3.0を利用させていただいています。

Gitのあるコミット間で変更されたファイルを一覧で取得したい

git diffコマンドのオプションである --name-only をつけてあげると変更ファイルの一覧を取得できます。

git diff --name-only [コミットID] [コミットID]

以下のように出力されるので、これを加工する場合はパイプでつないでいけば良いですね。

wp-includes/js/dist/list-reusable-blocks.min.js
wp-includes/js/dist/media-utils.js
wp-includes/js/dist/media-utils.min.js
wp-includes/js/dist/notices.js
wp-includes/js/dist/notices.min.js
wp-includes/js/dist/nux.js
wp-includes/js/dist/nux.min.js
wp-includes/js/dist/plugins.js
wp-includes/js/dist/plugins.min.js
wp-includes/js/dist/priority-queue.js
wp-includes/js/dist/priority-queue.min.js
wp-includes/js/dist/redux-routine.js
wp-includes/js/dist/redux-routine.min.js
wp-includes/js/dist/rich-text.js
wp-includes/js/dist/rich-text.min.js
wp-includes/js/dist/server-side-render.js
wp-includes/js/dist/server-side-render.min.js
wp-includes/js/dist/shortcode.js
wp-includes/js/dist/shortcode.min.js
wp-includes/js/dist/token-list.js
wp-includes/js/dist/token-list.min.js
wp-includes/js/dist/url.js
wp-includes/js/dist/url.min.js
wp-includes/js/dist/vendor/lodash.js
wp-includes/js/dist/vendor/lodash.min.js
wp-includes/js/dist/vendor/react-dom.js
wp-includes/js/dist/vendor/react-dom.min.js
wp-includes/js/dist/vendor/react.js
wp-includes/js/dist/vendor/react.min.js
wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js
wp-includes/js/dist/vendor/wp-polyfill.js
wp-includes/js/dist/vendor/wp-polyfill.min.js

おまけ

xargsの -I オプションを使うことで、標準入力で受け取った値をxargsに渡した引数のコマンドの任意の位置に展開することが可能です。 なので、git diff --name-onlyとxargs -Iを組み合わせることで、以下のようなことができます。

git diff --name-only c62ce56 52f858f | xargs -IIN scp IN aws-dev:/home/ec2-user

これはgit diffであるコミット間で変更されたファイルの一覧を標準出力してパイプでxargsに渡しています。 そして、パイプで受け取った標準入力は、xargsの-I オプションで任意の位置に展開してscpコマンドを実行してサーバーにファイルをコピーしています。 IN は自分で決めた値で、ここはXXXやYYYでも問題ありません。

CSSで画像をトリミングするためにobject-fitを使ってたら痛い目を見ました

久しぶりにHTMLコーディングをすることがありました。

大きい画像をCSSでトリミングして良い感じに表示したいなと思い、object-fit: cover; を使って良い感じに表示できてすごく便利になったものだなと感心してました。

で、あとから画像の比率がおかしいと指摘があり、ChromeやSafari・Firefoxで見ても崩れてないからキャッシュの仕業かなと思ってたんですが違いました。

ええ、コイツです。

f:id:ryskit:20190923231519p:plain

object-fit って便利なんですけど、IEが対応してくれてないのが残念だ。。。。

と思ってたら、JSでなんとか対応してくれるライブラリがありました。

object-fit-images ライブラリ

github.com

1. CSSを記述する

使い方は簡単で、ChromeやSafari・Firefoxはobject-fitを使えるので、その部分のCSSだけ外出しして以下のようにCSSを書いておきます。

.object-fit-img {
    object-fit: cover;
    font-family: 'object-fit: cover;';
}

大事なのは、 font-family: 'object-fit: cover;'; と記述している部分。 ここに書いているものがIEで適用されるので、ここを書き忘れるとIEでobject-fitが動きません。

これを書き忘れて、IEでも動かねーじゃねーかとハマってしまいました。。。ごめんなさい。

2. ofi.min.js ファイルを読み込む&有効にする

object-fit-images をダウンロードすると、 dist ディレクトリの下に、 ofi.min.js が入っているのでそれをheadタグ内で読み込めるように設定してください。

その後、JSファイルやスクリプトタグの中で、以下のように記述してください。

objectFitImages('img.object-fit-img');

これがないと、IEで有効にならないので注意。 img.object-fit-img は class名:  object-fit-img が付けられたimgタグに対して有効にするとしています。 ここはよしなに変更してください。

3. object-fitを使いたいimgタグにクラス名を付ける

1.で記述したクラス名をimgタグのクラス名に追加してください。 これでIEでもobject-fitが使えるようになります!

<img class="object-fit-img" src="...">

最後に

Web制作って久しぶりにやると楽しいけど、各ブラウザへの対応とかどこまで保証するかとか大変だなと思いました。

個人的にサーバー書いてるほうが楽しいな!

MVVMアーキテクチャでAPIからビール情報を取得して表示するコードを書くのに役立った記事をまとめておく

PUNK APIというBREW DOGのビール情報を返してくれるAPIがあり、それを使って簡単なデータ表示アプリをMVVMアーキテクチャで作ったときに役立った記事のリンクを備忘録としてまとめておく。

ライブラリは以下を利用した。

  • HTTP通信周り
    • OkHttp4
    • Retrofit2
  • Serde
    • GSON
  • DI
    • Koin
  • Databinding
  • Coroutine

先輩の記事

前々からプライベートでAndroidアプリを作ることが決まっていて、 Androidアプリ開発自体が初めてなので、イマイチどのライブラリを使えば良いか分からなかったり、そもそも使い方もどうやるんだろうと試行錯誤していて、 同じ会社の先輩に相談したりしていた。

たまたま、TwitterでRetrofit2やmoshiの記事を連投していると、その先輩がこういうので作ってみるといいよーとわざわざサンプルアプリを作って共有してくださってかなり理解も進んだような気がする!ありがとうございます!

そのサンプルアプリについての記事がこれ。

dev.classmethod.jp

Githubのリポジトリはこれ。

github.com

Android開発で書籍に出てくるようなサンプルアプリを作ったあと右往左往している状態なら、ひとまずリポジトリのコードを読んで試してみると良いかも!

Databindingを学ぶならこれ

たぶん技術書典で販売されていたものだと思う。 これは読んでいてすごく勉強になった。感謝!

booth.pm

Databinding + RecyclerViewで悩んだらこれ

記事ではないけど、サンプルアプリのコードを読めばいろいろ理解できるからおすすめ!

このリポジトリは良かった。

github.com

Retrofit2

square.github.io

qiita.com

Koin

Koinは先輩のコード読んだりドキュメント読んだり、記事読んだりしたぐらい。 やっぱり、ドキュメント読みつつ、他人のコードを読むのが一番理解できる。

insert-koin.io

qiita.com

Coroutine

ココらへんを読んだ気がする。まだまだ使いこなせてないから頑張るぞ!

qiita.com

qiita.com

最後に

自分のコードはある程度、書き直してからGithubのリポジトリにアップする予定です。

試して理解 Linuxのしくみの第4章を読んだ

blog.ryskit.com

前回の続き。

この4章で取り扱うのは「プロセススケジューラ」についてです。

この章で大事なのは以下のとおり。

  • 同時に何個のプロセスが実行していても、ある瞬間に論理CPU上で動作できるプロセスは1つだけ
  • 論理CPU上では、複数プロセスが、プロセスを順番に1ずつ動かして、1周したらまた一番目のプロセスから動かすラウンドロビン方式で動作している
  • 各プロセスはおおよそ等しい長さのタイムスライスを持つ
  • プロセス終了までの経過時間は、プロセス数に比例して増加
  • 1つのCPU上で同時に処理するプロセスは1つだけ
  • 複数プロセスが実行可能な場合、個々のプロセスを適当な長さの時間ごと(タイムスライス)にCPU上で順番に処理する
  • マルチコアCPU環境では、複数プロセスを同時に動かさないとスループットが上がらない
  • 単一論理CPUの場合と同様に、プロセス数を論理CPU数より多くしてもスループットは上がらない

実験プログラムを書いてVPS上で試したり、書籍に載っているグラフを見てなんとなく理解した気になっているが、 まだ自分の中に落とし込めていないように思う。

とりあえずわからなくても先に進むのが良いので、2周目読むときに復習兼ねて記事にまとめよう。

ノート

第4章 プロセススケジューラ

  • マルチコアCPUは、Linuxからは、1つのコアが1つのCPUとして認識される
  • プロセスは、ロードバランサ機能によって、システムの負荷に応じて複数の論理CPUをまたいで実行することがある
  • tasksetコマンド
    • コマンドライン引数で指定したプログラムを、「-c」オプションによってい指定した論理CPU上でのみ動作させる
  • 各プロセスは論理CPUを使っている間だけ処理が進捗し、論理CPU上でもう一方のプロセスが動作している間は進捗しない
  • 単位時間あたりの進捗は、プロセス数=1のおよそ半分。プロセス数=1の場合は1ミリ秒ごとに1%程度、プロセス数=2の場合は1ミリ秒ごとに0.5%程度
  • 処理完了までの経過時間は、プロセス数=1の場合のおよそ2倍
  • 同時に何個のプロセスが実行していようとも、ある瞬間に論理CPU上で動作できるプロセスは1つだけ
  • 論理CPU上では、複数プロセスが、プロセスを順番に1ずつ動かして、1周したらまた一番目のプロセスから動かすラウンドロビン方式で動作している
  • 各プロセスはおおよそ等しい長さのタイムスライスを持つ
  • プロセス終了までの経過時間は、プロセス数に比例して増加
  • コンテキストスイッチ
    • 論理CPU上で動作するプロセスが切り替わること
Linuxにおいて、必ずしも、foo()直後にbar()を実行するという保証はない
void foo(void)
{
  ...
  foo()
  bar()
  ...
}
プロセスの状態 意味
実行状態 現在論理CPUを使っている
実行待ち状態 CPU時間が割り当てられるのを待っている
スリープ 何らかのイベントが発生するのを待っている。イベント発生までのCPU時間は使わない
ゾンビ状態 プロセスが終了した後に親プロセスが終了状態を受け取るのを待っている
  • プロセスは生存中に、実行状態、実行可能状態、スリープ状態という複数の状態を行き来する
  • アイドルプロセス
    • 何もしない特殊なプロセス
    • 新たにプロセスが生成されるか、スリープしているプロセスが希少するまで無駄なループをする
    • CPUの特殊な命令を用いて論理CPUを休止状態にし、1つ以上のプロセスが実行可能状態になるまで消費電力を抑えた状態にする
  • 論理CPU上で一度に実行できるプロセスは1つだけ
  • スリープ状態においてはCPU時間を使わない

スループットとレイテンシ

  • スループット: 単位時間あたりの総仕事量。高いほど良い
  • レイテンシ: それぞれの処理の開始から終了までの経過時間。短いほど良い。
  • プロセスが動作中。実行待ちプロセスがないので、理想的な状態。ただしこの状態で次のプロセスが実行可能状態になると、2つのプロエスのレイテンシが両方とも長くなる
  • プロセスが動作中。実行待ちプロセスもあり。スループットは高いが、レイテンシが長くなる傾向にある

  • 経過時間: プロセスが開始してから終了するまでの経過時間。
  • 使用時間: プロセスが実際に論理CPUを使用した時間
  • 1つのCPU上で同時に処理するプロセスは1つだけ
  • 複数プロセスが実行可能な場合、個々のプロセスを適当な長さの時間ごとにCPU上で順番に処理する
  • マルチコアCPU環境では、複数プロセスを同時に動かさないとスループットが上がらない
  • 単一論理CPUの場合と同様に、プロセス数を論理CPU数より多くしてもスループットは上がらない

試して理解 Linuxのしくみの第3章を読んだ

blog.ryskit.com

の続き。

今日は第3章のプロセス管理を読んだ。

カーネルによるプロセス生成と削除の機能についてだが、仮想記憶の理解がないと詳しい理解が得られないということで、仮想記憶がない単純な場合をもとに話が進んだ。仮想記憶については5章で述べられるようだ。

プロセスに関しては、以前に「なるほどUnixプロセス」を読んでいたため、理解が早かった。

tatsu-zine.com

ただ、プログラムがどのようにメモリ上にマップされるかまでは把握してなかったので、 解説の図を見たり、readelfコマンド、/proc/[pid]/maps ファイルを実際に確認したので、 ざっくりこんなもんかと知れたのは良かった。

ノート

第3章 プロセス管理

  • Linuxにおいて、プロセス生成は2つの目的がある
    • 同じプログラムの処理を複数のプロセスに分けて処理する
      • fork()
    • 全く別のプログラムを生成する
      • execve()
  • fork() 関数
    • 発行したプロセスをもとに、新たにプロセスを1つ生成する
      • 親プロセス - 子プロセス
    • 流れ
      1. 子プロセス用のメモリ領域を作成して、親プロセスのメモリをコピーする
      2. 親プロセスと子プロセスは違うコードを時刻するように分岐する
      3. form()関数の戻り値が、親プロセスと子プロセスの間で異なることを利用する
  • execve() 関数
    • 流れ
      1. 実行ファイルを読み出して、プロセスのメモリマップに必要な情報を読み出す
      2. コードを含むデータ領域のファイル上オフセット、サイズ、およびメモリマップ開始アドレス
      3. コード以外の変数などデータ領域ついての上記と同じ情報
      4. 最初に実行する命令のメモリアドレス(エントリポイント)
      5. 現在のプロセスのメモリを新しいプロセスのデータで上書きする
      6. 新しいプロセスの最初の命令から実行開始する
  • https://qiita.com/zacky1972/items/ef4486e8a6d95edb68fd
    • CPUは レジスタ (register, processor register) と呼ばれる小規模な記憶装置を持っている
    • レジスターはメモリと比べて記憶容量が小さいが,その代わりとても高速に読み書きすることができる
  • Linuxの実行ファイルは Executable Linkable Format(ELF)というフォーマットを使用する
    • readelfコマンドで確認できる
      • 開始アドレスを得る場合は「-h」オプション
      • コードとデータのファイル内オフセット、サイズ、開始アドレスを得る場合は「-S」オプション
      • 「.text」なのがコード領域の情報、「.data」なのがデータ領域の情報
  • プログラム実行時に作成されたプロセスのメモリマップは、/proc/[pid]/mapsというファイルで得られる
cat /proc/29537/maps

55e16a5e9000-55e16a5f0000 r-xp 00000000 fc:02 1048708                    /bin/sleep
55e16a7f0000-55e16a7f1000 r--p 00007000 fc:02 1048708                    /bin/sleep
55e16a7f1000-55e16a7f2000 rw-p 00008000 fc:02 1048708                    /bin/sleep
55e16ac85000-55e16aca6000 rw-p 00000000 00:00 0                          [heap]
7f9cccce9000-7f9ccce5c000 r--p 00000000 fc:02 131771                     /usr/lib/locale/C.UTF-8/LC_COLLATE
7f9ccce5c000-7f9ccd043000 r-xp 00000000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd043000-7f9ccd243000 ---p 001e7000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd243000-7f9ccd247000 r--p 001e7000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd247000-7f9ccd249000 rw-p 001eb000 fc:02 1185282                    /lib/x86_64-linux-gnu/libc-2.27.so
7f9ccd249000-7f9ccd24d000 rw-p 00000000 00:00 0
7f9ccd24d000-7f9ccd274000 r-xp 00000000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd295000-7f9ccd2c6000 r--p 00000000 fc:02 131773                     /usr/lib/locale/C.UTF-8/LC_CTYPE
7f9ccd2c6000-7f9ccd2c7000 r--p 00000000 fc:02 131778                     /usr/lib/locale/C.UTF-8/LC_NUMERIC
7f9ccd2c7000-7f9ccd2c8000 r--p 00000000 fc:02 131781                     /usr/lib/locale/C.UTF-8/LC_TIME
7f9ccd2c8000-7f9ccd2c9000 r--p 00000000 fc:02 131776                     /usr/lib/locale/C.UTF-8/LC_MONETARY
7f9ccd2c9000-7f9ccd2ca000 r--p 00000000 fc:02 131782                     /usr/lib/locale/C.UTF-8/LC_MESSAGES/SYS_LC_MESSAGES
7f9ccd2ca000-7f9ccd2cb000 r--p 00000000 fc:02 131779                     /usr/lib/locale/C.UTF-8/LC_PAPER
7f9ccd2cb000-7f9ccd2cc000 r--p 00000000 fc:02 131777                     /usr/lib/locale/C.UTF-8/LC_NAME
7f9ccd2cc000-7f9ccd2cd000 r--p 00000000 fc:02 131770                     /usr/lib/locale/C.UTF-8/LC_ADDRESS
7f9ccd2cd000-7f9ccd2ce000 r--p 00000000 fc:02 131780                     /usr/lib/locale/C.UTF-8/LC_TELEPHONE
7f9ccd2ce000-7f9ccd2cf000 r--p 00000000 fc:02 131775                     /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
7f9ccd2cf000-7f9ccd46a000 r--p 00000000 fc:02 145331                     /usr/lib/locale/locale-archive
7f9ccd46a000-7f9ccd46c000 rw-p 00000000 00:00 0
7f9ccd46c000-7f9ccd473000 r--s 00000000 fc:02 137960                     /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f9ccd473000-7f9ccd474000 r--p 00000000 fc:02 131774                     /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
7f9ccd474000-7f9ccd475000 r--p 00027000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd475000-7f9ccd476000 rw-p 00028000 fc:02 1185270                    /lib/x86_64-linux-gnu/ld-2.27.so
7f9ccd476000-7f9ccd477000 rw-p 00000000 00:00 0
7ffd81948000-7ffd81969000 rw-p 00000000 00:00 0                          [stack]
7ffd819ad000-7ffd819b0000 r--p 00000000 00:00 0                          [vvar]
7ffd819b0000-7ffd819b2000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
  • プログラム終了には「_exit()」関数(内部的にはexit_group()システムコール)を呼ぶ
    • プロセスに割り当てていたメモリをすべて回収する

試して理解 Linuxのしくみの第2章を読んだ

blog.ryskit.com

の続き。

第2章では、CPUはユーザモードとカーネルモードを切り替えて処理を実行しているということを学んだ。

CPUのモード遷移の図や実験で試したプログラムのCPUのモードの遷移図は分かりやすい。

試したコマンドは、strace, sar

簡単なプログラムを書いて、「ユーザモードで動く場合」「カーネルモードで動く場合」のプロセスのモードの割合を確認した。

まだ難しくない内容なので、どんどん進めていこう。

ノート

第2章 ユーザモードで実現する機能

  • 各種プロセスは、プロセス生成、ハードウェアの操作など、カーネルの助けが必要なときにはシステムコールという手段によってカーネルに処理を依頼する
  • システムコール
    • プロセス生成、削除
    • メモリ確保、解放
    • プロセス間通信
    • ネットワーク
    • ファイルシステム
    • ファイル操作(デバイスアクセス)
  • システムコールを発行すると、CPUにおいて割込みというイベントが発生する
    • これにより、CPUではユーザモードからカーネルモードに依頼し、内容に応じてカーネルの処理が動く
    • 処理が終われば、ユーザモードに戻りプロセスの動作を継続する
  • straceの出力は、1つのシステムコール発行が1行に対応している
  • sarコマンドでプロセスがユーザモードとカーネルモードのどちらで実行しているかの割合を取得できる
  • sar -P ALL 1
  • allになっている行は全CPUの平均値
  • ユーザモードでプロセスを実行している割合は、%userと%niceの合計
  • CPUコアが、カーネルモードでシステムコールなどの処理を実行している時間の割合は、「%system」によって得られる
  • %idleはCPUコア上でプロセスもカーネルも動いていない状態
各行が1つのCPUに対応している
サーバーは1CPUなので、一つ

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.00      0.00      0.00      0.00      0.00    100.00
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00
loop処理実行後

Linux 4.15.0-50-generic (133-130-123-136)   06/10/19    _x86_64_    (1 CPU)

21:51:46        CPU     %user     %nice   %system   %iowait    %steal     %idle
21:51:47        all    100.00      0.00      0.00      0.00      0.00      0.00
21:51:47          0    100.00      0.00      0.00      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all    100.00      0.00      0.00      0.00      0.00      0.00
Average:          0    100.00      0.00      0.00      0.00      0.00      0.00
親プロセスのプロセスIDを得るシステムコールのループ処理実行後

sar -P ALL 1 1
Linux 4.15.0-50-generic (133-130-123-136)   06/10/19    _x86_64_    (1 CPU)

21:55:02        CPU     %user     %nice   %system   %iowait    %steal     %idle
21:55:03        all     46.46      0.00     53.54      0.00      0.00      0.00
21:55:03          0     46.46      0.00     53.54      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     46.46      0.00     53.54      0.00      0.00      0.00
Average:          0     46.46      0.00     53.54      0.00      0.00      0.00
  • %systemが数十のような大きな値に似合っている場合は、むやみにシステムコールを発行しているか、システム負荷が高すぎる場合が多い
  • straceに「-T」オプションをつけると、各種システムコールの処理にかかった時間をマイクロ秒の精度で採取できる
  • システムコールは、通常の関数呼び出しとは違い、直接呼び出せない
    • アーキテクチャ依存のアセンブリコードを使って呼び出す必要がある
    • この問題を解決するために、OSは内部的にシステムコールを呼び出すだけの、システムコールラッパーと呼ばれる一連の関数を提供している
    • https://twitter.com/_ryskit/status/1138069450014330881
  • glibcは、システムコールのラッパー関数を含む
    • ldd /bin/echo
    • ldd ppidloop
    • ldd /usr/bin/python3

試して理解 Linuxのしくみの第1章を読んだ

f:id:ryskit:20190609202759j:plain

「Linuxのしくみ」はOSやハードウェアについて、実際に手を動かして挙動を確認しながら学べる本です。

仕事で一緒に働いている方々が低レイヤに詳しいので、感化されてこの本を手に取った。 理由は、試して理解と書いてあったから。

手を動かさずに理解するのは得意じゃなく、手を動かして、どういった動きをするのか自分の目で確認しつつ学びたかったので、この本は僕にはちょうど良かった。

全部で8章で、1章の概要はすでに読んだからあと4週間もあれば試して少しは理解できる状態になっているのではないかと期待している。

実験プログラムの環境はUbuntu 16.04/x86_64環境なので、とりあえずConohaで月640円のVPS借りて試してみることにする。 ローカルで立ててもいいのだけれど、会社のPCしか持ち合わせていないことも考えて、いつでもログインできるようにしておきたかったからだ。

パラッと全体に目を通してみた感じだと図が多く、分かりやすそうといった印象。 とにかく、手を動かすしかない。

ノート

第1章 コンピュータシステムの概要

  • OSは各種プログラムをプロセス単位で実行する
  • 各プログラムは1つないし複数のプロセスから構成される
  • Linuxの重要な役割として、外部デバイスの操作がある
    • LinuxのようなOSがないと、各プロセスが独自にデバイスを操作するコードを書く必要がある
    • LInuxではデバイスを操作する処理をデバイスドライバというプログラムにまとめており、プロセスからデバイスには、このデバイスドライバを介してアクセスする
  • Linuxはハードウェアの助けを借りて、プロセスからデバイスに直セスアクセスできないようにしている
    • CPUには2つのモードがある
      • カーネルモード
        • カーネルモードの場合に、デバイスにアクセスできるようにする
        • デバイスドライバはカーネルモードで動作する
        • プロセス管理システム
        • プロセススケジューラ
        • メモリ管理システム
      • ユーザーモード
        • プロセスはユーザーモードで動作する
  • カーネルモードで動作する、OSの核となる処理をまとめたプログラムを「カーネル」と呼ぶ
  • プロセスは、カーネルが提供する機能を使いたければ、システムコールという特殊な処理を介してカーネルに依頼する

direnvの使い方をいつも忘れるからメモしておく

direnvの使い方をいつも忘れるので備忘録として書いておく。

direnvとは

特定のディレクトリに移動したときに設定ファイルに記載された環境変数を読み込んで有効化するツール。 とりあえず、まだインストールしてないのであればインストールするべし!便利なので!

インストール

Macの方はHomebrewで入れればいいと思います。

$ brew install direnv

上記以外でインストールする方はリポジトリのREADMEを読んでください。

github.com

使い方

特定のプロジェクトのディレクトリ配下で .envrc に環境変数を書き込む

$ echo "export AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ echo "export AWS_SECRET_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ echo "export AWS_DEFAULT_REGION=ap-northeast-1" >> .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

.envrcの内容を許可する

このままだと .envrc はブロックされたままなので以下のコマンドで許可してあげる。

$ direnv allow .
direnv: loading .envrc
direnv: export +AWS_ACCESS_KEY_ID +AWS_DEFAULT_REGION +AWS_SECRET_ACCESS_KEY

これで、環境変数がロードされたはず。 ためしに、環境変数を出力してみよう。

$ echo $AWS_DEFAULT_REGION
ap-northeast-1

はい、ちゃんと出力されてます。

特定のディレクトリから抜けると環境変数もアンロードされる

今のディレクトリから一つ上の階層に移動してみよう。

$ cd ..
direnv: unloading

.envrcに書き込んだ環境変数がアンロードされました。 確認するために出力してみます。

$ echo $AWS_DEFAULT_REGION

はい、何も出力されなくなりました。

これでいちいち環境変数を読み込んだりする作業が楽になりますね!

注意

.envrc はGithub等にコミットするのはやめましょう。 リポジトリが公開されていると、AWSのシークレットキーなどが盗まれて悪用されちゃうので。