2026/08/21

C 言語の変数定義

C 言語の変数定義は宣言部に若干クセがあるのでまとめました。

ローカル変数

変数のスコープが宣言されたブロックの内側に限定される変数です。 ブロックとは {} の内側などで、一番判りやすいのは関数の宣言部分です。

以前は変数宣言はブロックの先頭のみで可能でしたが ISO/IEC 9899:1999 (C99) から任意の場所の変数宣言が可能となっています。

  1int     foo(void)
  2{
  3
  4    int     i;
  5
  6    i = 0;                  /* ローカル変数 */
  7
  8    {
  9        int     j;          /* ローカル変数 */
 10            :
 11    }
 12
 13    /* ここでは j は参照できない */
 14
 15}
    
ローカル変数の特徴を以下に示します。
  • スコープ (有効範囲)は宣言されたブロック内に限られます。
  • 関数の呼び出しごとに新しく確保され関数から戻るときに破棄されます。
  • 実体は基本的に後述するスタック上に置かれます。
  • 初期化しないと値は不定で内容は保証されていません。

グローバル変数

関数の外で宣言する変数です。

  1int     status;                 /* グローバル変数 (初期値は 0) */
  2
  3int     foo(void)
  4{
  5
  6    if(status == 1){            /* ファイル内のどこからでも参照できる */
  7        :
    
グローバル変数の特徴を以下に示します。
  • スコープはファイル全体です (extern を宣言する事で他ファイルからもアクセス可能)。
  • プログラム開始時に確保され終了するまで存在し続けます。
  • 明示的に初期化しなくても自動的に 0 で初期化されます。
  • 実体は実行ファイル内の「データ領域」に配置されます。
グローバル変数のうち、初期値を持つものは .data セグメント、 初期値を持たないもの (自動で初期化されるもの) は .bss セグメントに割り当てられるのが一般的です。

メモリレイアウトとスタック

典型的なプロセスのメモリ空間はアドレスの低い方から高い方へおおよそ次のように並びます。

テキスト領域
データ領域 (.data)
BSS 領域 (.bss)
ヒープ
スタック
機械語コード
初期化済みグローバル / static 変数
未初期化グローバル / static 変数
malloc(3) で動的に確保される領域
関数ごとに利用される領域

ヒープとスタックが互いに向かって伸びていくため、両方が使いすぎて衝突すると「スタックオーバーフロー」などの問題が起きます。

スタックの動作(スタックフレーム)

関数を呼び出すたびに、スタック上に「スタックフレーム」と呼ばれる領域が積まれます。中身はおおよそ次のようなものです。

  • 呼び出し元に戻るためのリターンアドレス
  • 呼び出し元のベースポインタの退避値(スタックフレーム同士を辿るため)
  • 引数(レジスタ渡しの場合もあります)
  • その関数のローカル変数

具体的なスタック利用の流れ

  1int     add(int a, int b)
  2{
  3
  4    int result = a + b;     /* ローカル変数はこの関数のスタックフレームに置かれる */
  5
  6    return result;
  7    
  8}
  9
 10int     main(void)
 11{
 12
 13    int x = 3,
 14          y = 4;
 15    int z = add(x, y);      /* add() 呼び出し時に新しいスタックフレームが積まれる */
 16
 17    printf("%d\n", z);
 18
 19    return 0;
 20    
 21}
            
  1. main() が呼ばれるとスタックに main() 用のフレームが積まれ(プッシュ)、 スタック上に変数 xyz が確保されます。
  2. 関数 add(x, y) が呼ばれるとスタックにリターンアドレスなどを積んだ上で さらに add() 用のフレームが積まれ、引数 ab、変数 result が確保されます。
  3. add()return するとそのフレームはスタックから取り除かれ(ポップ)、 制御は main() に戻ります。 add() のローカル変数 (abresult) はこの時点で消滅します。
  4. main()return すると、main() のフレームも取り除かれます。
この様な構造のため関数 add() で定義された変数 resultadd() の外では参照できませんし、 参照しても値は保証されていません。

ダングリングポインタによる BUG

関数 add() を少し変更して文字列を操作する関数に変えてみます (例示のためなのでチェック処理やエラー処理などは実施していません)。

  1char    *add(char *a, char *b)
  2{
  3
  4    char    result[1024];               /* ローカル変数はこの関数のスタックフレームに置かれる */
  5
  6    snprintf(result, sizeof(result), "%s%s", a, b);
  7
  8    return result;
  9
 10}
 11
 12int     main(void)
 13{
 14
 15    char    *x = "foo",
 16            *y = "bar";
 17    char    *z = add(x, y);         /* 関数 add() のローカル変数を参照 */
 18
 19    printf("%s\n", z);
 20
 21    return 0;
 22    
 23}
            
関数 add() はローカル変数 result のアドレスをリターンしていますが、 前述の通り result はスタック上に確保された変数のため add() のリターン後は値は保証されません。 リターンした値はスタックのアドレスを示していますが値は保証されていません。 そのため z の値は不定となってしまいます。 関数からローカル変数をリターンする場合は必ず値をリターンする必要があります。 C 言語の仕様として配列の名前を参照した場合は配列の先頭要素を指すアドレスとして扱われますので特に注意が必要です。 この様にローカル変数のアドレスをリターンする事をダングリングポインタと呼びます。

再帰とスタックオーバーフロー

このLIFO (Last In First Out) 構造のおかげで、 関数呼び出しが何段ネストしても各関数は自分専用の変数領域を持てますし、 再帰呼び出しでも呼び出し回数分のフレームが積み重なるだけで正しく動作します。 逆に言うと、再帰が深すぎたり、ローカルに巨大な配列を確保しすぎたりすると、 このスタック領域を使い切って「スタックオーバーフロー」を起こします。

  1void    recurse(int n)
  2{
  3
  4    char    buf[1024 * 1024];           /* 1関数呼び出しごとに 1MB 消費 */
  5
  6    recurse(n + 1);                     /* 再帰呼び出し (すぐにスタックが尽きる) */
  7
  8}
            
とは言っても再帰呼び出しする場合は staic 変数は BUG の原因となるので、 再帰呼び出しの可能性がある場合は static 変数は使わない方が良いです。

以下は static 変数を利用したために不正な値となってしまう階乗計算処理です。
  1int     count(int n)
  2{
  3
  4    static  int total = 0;              /* すべての再帰で共有される */
  5    
  6    total *= n;
  7    if(n > 0)
  8        count(n - 1);                   /* 再帰呼び出し */
  9
 10    return total;
 11
 12}
 13
 14int     main(int argc, char *argv[])
 15{
 16
 17    printf("%d\n", count(atoi(argv[1])));
 18
 19    return 0;
 20
 21}
            
この様な BUG の温床になってしまう事もあるので、 特に最近では再帰呼び出し自体を悪だとする風潮もある様ですが 再帰呼び出し便利なんですよね...ついつい使ってしまいます。

static 宣言

C 言語の static 修飾子は文脈によって複数の意味と効果を持っているので初心者の頃は混乱しがちです。 そもそも複数の効果を持つ修飾子を同じ名前にするな...と Dennis に言いたい。

関数内の static 変数

関数の内部での static 修飾子は記憶クラスを定義します。 static 指定の変数を定義すると変数は前述のデータ領域に置かれ関数が終了した後も値が保持されますが、 変数のスコープはローカル変数同様にブロックの内部のみとなっています。 以下の関数 foo() では最初に呼ばれた時のみ変数 i が 0 で初期化され、 呼び出し毎に i がインクリメントされます。 2 回目の呼び出しでは i は 2 に、10 回目の呼び出しでは i は 10 となります。 当然ですが関数 foo() の外側では i は参照できません。

  1int     foo(void)
  2{
  3
  4    static  int     i = 0;          /* 変数領域は .data に確保され初期値として 0 が設定される */
  5
  6    i++;
  7
  8}
        

ファイルのグローバル変数

グローバル変数への static 修飾子は変数のスコープを定義します。 static 指定の変数はスコープがファイル内に制限されます。 static 修飾子を指定しない変数は他のファイルで extern を指定する事で参照する事ができますが、 static 修飾子を指定された変数は extern 指定されても他のファイルからは参照できません。

  1/* ファイル foo.c */
  2static  int     result;         /* 他のファイルから参照不可 */
  3        int     status;         /* 他のファイルから参照可能 */
  4
  5    :
  6
  7/* ファイル bar.c */
  8extern  int     status;         /* foo.c の status を参照 */
  9extern  int     result;         /* foo.c の result は参照できない (通常はコンパイル時にリンクエラーとなる) */
 10
 11    :
        

関数の指定

関数の宣言に static 修飾子を指定した場合はファイルのグローバル変数同様関数のスコープを定義します。 static 修飾子を指定した関数はスコープがファイル内に制限されます。 static 修飾子を指定しない関数は extern 指定する事で他のファイルからも呼び出す事ができますが、 static 修飾子を指定された関数は extern 指定されても他のファイルからは呼び出せません。

まとめ

グローバル変数は値を保持しグローバルに参照できるので便利な変数です。 しかし、多用すると処理全体の見通しが悪くなったり関数の独立性が阻害されてしまうので、 どうしても必要な場合に限ったほうが良いでしょう。


ローカル変数 グローバル変数
宣言位置 関数 / ブロック内 関数外
スコープ そのブロック内のみ ファイル全体 (extern で他ファイルも可)
記憶域期間 自動 (関数終了で消滅) 静的 (プログラム終了まで存在)
確保場所 スタック (static 以外) データ / BSS 領域
初期値 不定 自動的に 0
主なリスク スタックオーバーフロー、ダングリングポインタ 名前衝突、意図しない状態共有によるバグ、マルチスレッドでの競合

2026/08/20

Cシステムコールとライブラリ関数

C 言語からコールする関数には大きく分けて システムコールライブラリ関数 の2種類があります。
どちらも関数呼び出しの見た目をしているため区別がつきにくいのですが、実行時の仕組み、コスト、権限モデルはまったく異なります。
そこで、両者の定義と違いを整理したうえで、システムコール実行時に発生するコンテキストスイッチが OS やアプリの性能にどのような負荷・影響を与えるかを簡単に解説します。

システムコール

システムコールとは、ユーザー空間で動作するプログラムがカーネルの機能を利用するための唯一の手段です。
man 2 open のようにマニュアルのセクション 2 に分類されています。

システムコールには open(2)read(2)write(2)socket(2) などがあります。

CPU には一般に実行できる命令や参照できるメモリ領域に制限のある「ユーザーモード」と、 すべての命令、メモリ空間、I/O ポートにアクセスできる「カーネルモード (特権モード)」の2つの動作モードがあります。 Unix や Linux を始めとするモダンな OS では、アプリケーションプログラムは基本的にユーザーモードで動作していて、 ディスクへの直接アクセスやプロセス管理などの危険な操作はハードウェア的に禁止されています。 そこで、こうした操作が必要な場合は CPU に用意された特別な命令を実行して、 CPU のモードをユーザーモードからカーネルモードへ切り替える必要があります。 Unix や Linux においてこの切り替えを実行するのがシステムコールです。

ライブラリ関数

ライブラリ関数は C 標準ライブラリ (libc) やその他のライブラリが提供するユーザー空間だけで完結する通常の関数です。
man 3 printf のようにマニュアルのセクション3に分類されています。

ライブラリ関数の分類

ライブラリ関数は大きく 2 種類に分類できます。
  1. システムコールを呼び出さない純粋なライブラリ関数
  2. strlen(3)sin(3) などで、これらは文字列操作や CPU 上での計算処理のみで完結しカーネルには影響しません。

  3. 内部でシステムコールをラップしているライブラリ
  4. fopen(3)fread(3)printf(3) などで、 これらは最終的に内部で open(2)read(2)write(2) といった システムコール発行していますが、 その前後でバッファリング、書式変換、エラー処理の付加などユーザー空間側の便利な処理を行っています。

たとえば fwrite(3) は、呼び出しのたびにシステムコール write(2) を発行するわけではなく、 FILE 構造体が持つユーザー空間のバッファにデータを蓄積して、バッファが一杯になった時、 あるいは fflush(3)fclose(3) が呼ばれた時にまとめて write(2) システムコールを発行する事で、システムコール呼び出し回数を減らし性能を大きく向上させています。

ライブラリ関数には fopen(3)fread(3)fwrite(3) などがあります。

コンテキストスイッチ

コンテキストスイッチとは、CPUがあるコンテキストから別のコンテキストへ切り替える処理全般を指します。

コンテキストスイッチの種類

コンテキストスイッチには大きく分けて 2 種類あります。
  1. モード切り替
  2. 前述したカーネルモードユーザーモードの切り替えです。
    このタイプのコンテキストスイッチはシステムコール発行や割り込み・例外発生時に起こります。 同一プロセス内での切り替えであり、後述する「プロセス切り替え」よりコストは小さいですがそれなりに負荷やコストがかかります。

  3. プロセス切り替え
  4. OS のスケジューラがプロセス/スレッドの実行を中断して別のプロセス/スレッドに実行を渡す処理です。 タイムスライスの満了、I/O 待ちによるブロック、割り込みによるプリエンプションなどで発生します。 ページテーブル (= アドレス空間) 自体が切り替わるためモード切り替えよりもさらにコストが大きくなります。

システムコールは、必ずモード切り替えを伴います。 更にそのシステムコールの処理が入出力など I/O 待ちなどでブロックが発生する場合には さらにプロセス切り替えを誘発することもあります。

コンテキストスイッチのシステム負荷・影響

モード切り替え(システムコール発行)のコスト要因

システムコールを 1 回発行するだけでも以下のような処理コストが発生します。
  • レジスタの退避・復元
  • 呼び出し元の CPU レジスタ一式をカーネルスタックへ保存し復帰時に復元します。

  • 特権レベルの遷移
  • CPU 内部の カーネルモードへの遷移、および復帰時のユーザーモードへの遷移で、CPU の内部的な検証・準備処理を伴います。

  • パイプラインフラッシュ・投機実行のリセット
  • モード遷移命令は CPU の命令パイプラインをある程度フラッシュさせ、 投機実行による先読みの効果を失わせます。

他にも TLB (Translation Lookaside Buffer) やキャッシュの汚染(cache pollution)、 セキュリティ機構によるオーバーヘッドなど、様々な影響があります。

これらの要因により、単純なシステムコール 1 回の呼び出しコストは、通常の関数呼び出しと比較して 100 から 1000 倍以上のコストがかかります。

プロセス/スレッド切り替えのコスト要因

システムコールがブロッキング I/O などで待ち状態に入ると、 OS のスケジューラは別のプロセス/スレッドを実行するためにプロセス切り替えを行いますが、 これには前述のモード切り替えのコストに加えて以下のコストが上乗せされます。
  • ページテーブルの切り替え
  • アドレス空間が完全に入れ替わるためTLB 全体が無効化されます。

  • スケジューラの実行コスト
  • 次に実行すべきプロセス / スレッドを選択するためのスケジューラの処理が実行されます。

  • キャッシュの入れ替え
  • キャッシュに載っていた前プロセスのデータ・命令が新しいプロセスのアクセスによって徐々に追い出される事により、 キャッシュミス率が一時的に急増して実行速度が低下します。

これらの処理が実行されるため、プロセス切り替えのコストはモード切り替え単体よりもさらに大きくなってしまいます。

頻繁なシステムコールがもたらすシステム全体への影響

例えばアプリケーションが 1 バイトずつ read(2)write(2) システムコールを呼び出すような設計をすると、 以下のような問題が顕在化します。
  • スループットの低下
  • システムコール自体のオーバーヘッドがボトルネックとなり本来の I/O 帯域を活かせなくなります。

  • CPU 使用率の増加
  • ユーザー時間ではなくシステム時間として CPU 時間が消費されてしまうので、システムの実行時間が高くなります。

  • キャッシュ効率の悪化
  • 頻繁なモード切り替えにより CPU キャッシュのヒット率が下がり、 システムコールを呼ばないコードパスの実行速度まで間接的に低下し、結果としてシステム全体のパフォーマンスが低下します。

  • スケーラビリティの低下
  • マルチコア環境では頻繁なシステムコールがカーネル内部の共有データ構造の競合を引き起こすので、 コア数を増やしても性能が線形にスケールしない原因となり得ます。

  • レイテンシのジッター増加
  • 割り込みやシステムコールによるコンテキストスイッチが頻発すると、リアルタイム性が求められる処理でジッターが増加します。

まとめ

システムコールはカーネルが提供するハードウェア資源にアクセスするための唯一の方法で、 呼び出しの都度、CPU のコンテキストスイッチが発生します。 一方ライブラリ関数は、多くの場合ユーザー空間だけで完結し、 内部でシステムコールをラップしている場合でもバッファリングなどによって呼び出し回数を抑える設計がされています。 プログラムの要件や仕様などでライブラリ関数では機能が不十分だったり、 特殊な制御が必要な場合以外はライブラリ関数を利用するのが好ましいです。

付録

具体例による比較を実施してみました。実行時間の計測はどちらも Intel CPU 上で稼働する Linux にて実施しました。

システムコールを利用する場合

1 バイト書き込むたびにシステムコール (=モード切り替え) が発生するため、コンテキストスイッチのコストが積み重なり著しく遅くなります。
  1#include <unistd.h>
  2#include <fcntl.h>
  3
  4int main(void)
  5{
  6
  7  int       fd,
  8                i;
  9
 10    if((fd = open("out.txt", O_WRONLY | O_CREAT, 0644)) >= 0){
 11        for(i=0; i<100000; i++)
 12            write(fd, "x", 1);
 13        close(fd);
 14    }
 15    
 16  return 0;
 17
 18}
        
$ time ./syscall

real    0m0.124s
user    0m0.018s
sys     0m0.106s
        

ライブラリ関数を利用する場合

ユーザー空間でバッファリングしてからまとめて write(2) を呼ぶため システムコール発行回数が数百分の一以下に抑えられ高速に動作します。
  1#include <stdio.h>
  2int main(void)
  3{
  4
  5    int     i;
  6  FILE  *fp;
  7
  8    if((fp = fopen("out.txt", "w"))){
  9        for(i=0; i<100000; i++)
 10            fputc('x', fp);
 11        fclose(fp);
 12    }
 13  
 14  return 0;
 15
 16}
        
$ time ./library

real    0m0.003s
user    0m0.001s
sys     0m0.001s
        

2023/10/05

みずほ銀行 (再訪)

以前作成した みずほ銀行のオンラインバンキングで振り込みをする場合に必要な 第2暗証番号から指定された4桁の番号を見つけ出すためのスクリプト を、 最新のご利用カード (アプリ版) に対応させてみました。 こちらも awk (1) だけで書いたのでどんな環境でも動作する筈です。
第1引数は必須でご利用カード (アプリ版) で生成された第2暗証番号を指定します。
第2引数で番号を指定するか端末から入力すると対応する数字が表示されます。
こんなピンポイントでしか役立たないスクリプトを利用する人がいるかどうか不明ですが折角なので公開だけはしてみます。
(そもそもアプリで振り込めば第2暗証番号の入力自体不要なんですよね...)

  1#!/bin/sh
  2#
  3
  4myname=$(basename $0)
  5
  6error()
  7{
  8
  9    echo "$*" 1>&2
 10
 11    exit 255
 12
 13}
 14
 15if [ -n "${1}" ]
 16then
 17    if [ ${#1} -eq 6 ]
 18    then
 19        awk -v "base=${1}" '
 20            BEGIN{
 21                if("'$2'")
 22                    num = "'$2'"
 23                else{
 24                    printf "Input number: "
 25                    getline num < "/dev/tty"
 26                }
 27                split(num, nums, "");
 28                for(i=1; i<=4; i++)
 29                    printf "%s ", substr(base, nums[i], 1)
 30                print
 31            }
 32        ' < /dev/null
 33    else
 34        error "${myname}: base is 6 digit."
 35    fi
 36else
 37    error "Usage: ${myname} base [nnnn]"
 38fi
    

2021/05/06

/dev を復旧する

とある事情で Linux マシンの /devを殆ど全削除してしまったので復旧してみた。

chroot 環境で作業するスクリプトが chroot 環境を抜けるときに後始末として環境内の /dev等を削除しているのだが、 スクリプトの不具合で chroot 環境から抜けた後(むしろ不具合で chroot できなかった場合) にも /dev の削除処理が 動作してしまったので実環境の /dev が根こそぎ削除されてしまうという...何ともアレな BUG を踏み抜いた次第。

今回はたまたま同じ構成の Linux マシンがもう一台手元にあったので、 そちらの /dev 環境から情報を取得して mknod を実行するためのコマンドライン引数を取得する処理を作成した。

デバイス名やメジャー番号、マイナー番号は /dev 以下の全エントリの詳細情報を find (1) を利用して ls (1) を実行する事で取得し、 その出力を awk (1) を利用して加工し mknod (1) のコマンドライン形式として出力している。
作成するデバイスノードのオーナーとグループは ls (1) の出力から chown (1) コマンドを実行して設定している。 アクセス権限は ls (1)の出力は直接 chmod (1) コマンドで利用できないので、 stat (1) コマンドの出力書式に --format '%04a' を指定する事で 4桁の8進数として取得した値を利用して chmod (1) コマンドで設定している。

stat (1)--format オプションは GNU による独自拡張なので FreeBSD や macOS の純正 stat (1) では動作しないと思う。

/dev には他にもディレクトリ、シンボリックリンクが存在しているので ls (1) の出力からエントリーのタイプを判断し、 ディレクトリの場合は mkdir (1)、シンボリックリンクの場合は ln (1) を適宜実行している。

  1#!/bin/sh
  2
  3find /dev -exec ls -l {} \; |
  4    awk '
  5        # ディレクトリ作成
  6        function mkdir(name) 
  7        {
  8            "dirname " name | getline dirname
  9            if(dirname != "/dev")
 10                printf("mkdir -p %s\n", dirname)
 11            return dirname
 12        }
 13
 14        {
 15            if(NF > 3){
 16                # エントリーの種類取得
 17                type = substr($1, 1, 1)
 18                if(type == "c" || type == "b"){
 19                # デバイスノードの場合
 20                    # 親ディレクトリ作成
 21                    mkdir($NF)
 22                    # 権限取得
 23                    "stat --format '%04a' " $NF | getline mode
 24                    # コマンド出力
 25                    printf "if [ ! -%s %s ]; then  rm -f %s; mknod %s %s %d %d; fi; chown %s:%s %s; chmod %d %s\n",
 26                        type, $NF, $NF, $NF, type, $5, $6, $3, $4, $NF, mode, $NF 
 27                } else if(type == "d"){
 28                # ディレクトリの場合
 29                    # 権限取得
 30                    "stat --format '%04a' " $NF | getline mode
 31                    printf "if [ ! -%s %s ]; then rm -f %s; mkdir -p %s; fi; chown %s:%s %s; chmod %d %s\n",
 32                        type, $NF, $NF, $NF, $3, $4, $NF, mode, $NF 
 33                } else if(type == "l"){
 34                # シンボリックリンクの場合
 35                    # 親ディレクトリ作成
 36                    cwd = mkdir($(NF - 2))
 37                    printf "if [ ! -L %s ]; then rm -f %s; (cd %s; ln -s %s %s); fi\n",
 38                        $(NF-2), $(NF-2), cwd, $NF, $(NF-2)
 39                } else {
 40                    print type " " $0 > "/dev/stderr"
 41                }
 42            }
 43        }
 44    '
    

情報を取得するマシンでこのスクリプトを実行すると /dev を復元するためのコマンドが生成されるので、 /dev 環境がなくなってしまったマシンに転送して実行する事で /dev が復元できる。

必要に応じてその場で適当に作ったスクリプトなので完全無保証。
自分の場合は復旧できて今でもちゃんと動作している。

2020/07/22

awk による IPv4 アドレスのマッチ処理

標準入力から入力されるテキストデータから IPv4 アドレスを awk(1) を利用して抽出する。
その際に CIDR 形式でないアドレスに関しては末尾に "/32" を付与して CIDR 形式にして出力する。

  1awk '{
  2    if(match($0, /([[:digit:]]{1,3}\.){3}[[:digit:]]{1,3}(\/[[:digit:]]{1,2})?/))
  3        print match((ip = substr($0, RSTART, RLENGTH)), /\/[[:digit:]]{1,2}/) ? ip : ip "/32";
  4}'
    

1 行目の match() 関数で入力データから正規表現を利用して IPv4 アドレスを抽出し、 2 行目の substr() 関数を利用してマッチした範囲を切り出して IPv4 アドレスを変数 ip に格納している。 2 行目の match() 関数で変数 ip に格納された IPv4 アドレスにに CIDR 部分があるかを調査し、 CIDR 部がない場合は "/32" を付与して出力している。

IPv4 アドレスは "192.0.2.1" の様に「0 から 9 までの数字 1 桁から 3 桁が "." を挟んで 4 組連続する」形式で、 CIDR は "/24" の様に「"/" に続いて 0 から 9 までの数字 1 桁から 2 桁」なので、 正規表現は

[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(\/[0-9]{1,2})?
となるが、 煩雑になるので
([0-9]{1,3}\.){1,3}[0-9]{1,3}(\/[0-9]{1,2})?
とまとめ、 更に数字部分を POSIX クラスに置き換えて
([[:digit:]]{1,3}\.){1,3}[[:digit:]]{1,3}(\/[[:digit:]]{1,2})?
とした。

入力データ中の IPv4 アドレスが重複する可能性がある場合は、直接出力せず一度連想配列に格納する事で uniq(1) 相当の処理も可能となる。

  1awk '{
  2    if(match($0, /([[:digit:]]{1,3}\.){3}[[:digit:]]{1,3}(\/[[:digit:]]{1,2})?/))
  3        list[match((ip = substr($0, RSTART, RLENGTH)), /\/[[:digit:]]{1,2}/) ? ip : ip "/32"] = 1;
  4} END {
  5    for(i in liset)
  6        print i;
  7}'
    

2019/10/31

exif 情報を利用した画像の整理スクリプト

iPhone で撮影した画像を macOS 標準の イメージキャプチャ などを利用して Mac に取りこんだ後、 exif 情報を参照して画像ファイルを整理するスクリプトです。
利用するためには exiftool が必要となりますので、Homebrew または MacPorts を利用して導入して下さい。

macOS 10.14.6 (Mojave) での動作を確認していますが、exiftool 以外は POSIX に準拠したコマンドしか使っていないので、 exiftool さえ準備できれば他のバージョンの macOS はもちろん、FreeBSD や Linux 上でも稼働すると思います。

  1#!/bin/sh
  2# 検索する exif タグ
  3tag="DateTimeOriginal ModifyDate CreateDate"
  4
  5# 生成する画像ファイル名に追加する文字列
  6id="_iOS"
  7
  8# 画像ファイル格納先ディレクトリ
  9base=${HOME}/CameraFiles
 10picture="Pictures"
 11movie="Movies"
 12unknown="Unknown"
 13
 14#
 15# exif 情報から画像の作成日を取得
 16#   $1: ファイル
 17#
 18getDate()
 19{
 20
 21    local   _t _d
 22
 23    for _t in ${tag}
 24    do
 25        _d=$(exiftool -${_t} "${1}")
 26        if [ -n "${_d}" ]
 27        then
 28            echo ${_d} | awk '{ print $(NF-1), $(NF)}' | sed -e 's/:/ /g' -e 's/\..*//g'
 29            return
 30        fi
 31    done
 32
 33}
 34
 35#
 36# 格納先のファイル名を生成
 37# ファイル名は ${base}/TYPE/YYYY/MM/YYYYMMDD_HHMMSSXXX_iOS.EXT の形式
 38# XXX は 000 から 999 までを自動で採番する
 39#   $1: 格納先ディレクトリ
 40#   $2: 日付 (YYYYMMDD)
 41#   $3: 時間 (YYYYMMDD)
 42#
 43getName()
 44{
 45
 46    local   _n _nn
 47
 48    _n=1
 49
 50    while [ ${_n} -le 999 ]
 51    do
 52        _nn=$(printf "%03d" ${_n})
 53        if ls "${1}/${2}_${3}${_nn}${id}"* > /dev/null 2>&1
 54        then
 55            _n=$((_n + 1))
 56        else
 57            break
 58        fi
 59    done
 60
 61    echo "${1}/${2}_${3}${_nn}${id}"
 62
 63}
 64
 65#
 66# メイン処理
 67#
 68ls -1 *jpg *png *mov | while read i
 69do
 70    # ファイルの拡張子取得
 71    ext="${i##*.}"
 72
 73    # 拡張子でサブディレクトリを指定
 74    if [ "${ext"} = "mov" ]
 75    then
 76        type="${movie}"
 77    else
 78        type="${picture}"
 79    fi
 80
 81    # exif 情報から作成日を取得
 82    set -- $(getDate "${i}")
 83    year=${1}
 84    month=${2}
 85    day=${3}
 86    time=${4}${5}${6}
 87
 88    if [ -n "${year}" ]
 89    then
 90        # 作成日が取得できた場合
 91        dst="${base}/${type}/${year}/${month}"
 92        name="$(getName "${dst}" "${year}${month}${day}" "${time}").${ext}"
 93    else
 94        # 作成日が取得できない場合
 95        dst="${base}/${type}/${unknown}"
 96        name="${dst}/${i}"
 97    fi
 98
 99    # 格納先ディレクトリ作成
100    mkdir -p "${dst}"
101    # ファイル格納
102    cp "${i}" "${name}"
103done
    

2019/04/18

CentOS 7 のコンソール画面でキーマップを変更する

キーボードのキーが押下されるとキーコードと呼ばれる一意の値がシステムに通知される。
キーマップはこのキーコードに対して文字を定義するためのファイルで、 システム標準のキーマップは /lib/kbd/keymaps/legacy/i386/qwerty に格納されている。
CentOS 7 のコンソール画面のキーマップ情報は /etc/vconsole.conf ファイルに格納されていて、 起動時に自動で読み込まれコンソール画面のキーマップが設定される。

$ cat /etc/vconsole.con
KEYMAP="us"
FONT="latarcyrheb-sun16"
    

例えば US 配列のキーボードで邪魔な Caps Lock を Control に変更したい場合は Caps Lock のキーコードを調べて押下された時に Control が入力される様なキーマップを用意すれば良い。
キーマップは showkey コマンドで調査する事ができる。
showkey コマンドは画面に表示されている通り最後の入力の10秒後に終了する。

$ showkey
kb mode was UNICODE
[if you are trying this under X, it might not work
since the X server is also reading /dev/console ]

press any key (program terminates 10s acter last keypress)...
keycode 58 press                      ここで Capl Lock キーを押下
keycode 58 release                    ここで Capl Lock キーを離す
$
    
結果から Caps Lock キーのキーコードは 58 なのでキーコード 58 に対して Control を入力するキーマップを作成する。 キーマップファイルは他のキーマップファイルを読み込む事ができるので、オリジナルの us.map を読み込んで Caps Lock だけを変更する。 この時、Shift + Caps Lock の場合は Caps Lock としてみる。
キーマップファイルは gzip 形式で圧縮する必要があるので圧縮し、所定のディレクトリに格納する。
# cat << EOF | gzip -c > /lib/kbd/keymaps/legacy/i386/qwerty/us-nocaps.map.gz
include "us.map"

keycode 58 = Control
    shift keycode 58 = Caps_Lock
EOF
    

キーマップファイルの準備ができたら loadkeys コマンドでキーマップを読み込み挙動を確認する。

# loadkeys /lib/kbd/keymaps/legacy/i386/qwerty/us-nocaps.map.gz
Loading /lib/kbd/keymaps/legacy/i386/qwerty/us-nocaps.map.gz
    
Caps Lock キーが Control に、Shift + Caps Lock キーが Caps Lock になる事が確認できたら、 起動時にキーマップが読み込まれる様に /etc/vconsole.conf ファイルを編集する。
# cp /etc/vconsole.conf /etc/vconsole.conf.orig
# sed '/KEYMAP/ s/us/us-nocaps/' /etc/vconsole.conf.orig > /etc/vconsole.conf
    
これで再起動しても Caps Lock は Control となる。

2019/02/15

時間の範囲を指定してログを抽出する

syslog や apache のログファイルなどから時間の範囲を指定してログを抽出したい場合、 ログファイルの時間表記をそのまま比較するのは面倒なので一度 epoch に置き換えて比較すると楽だ。

現在時刻から epoch は date (1) のフォーマット指定 '+%s' を利用する事で取得できるが、 指定した時刻から epoch を取得する汎用的な手法は現状では存在しないので今回は GNU date の '-d' オプションを利用する。

例として combined 形式の apache のログの時間範囲を抽出してみる。
combined 形式の apache のログは以下のフォーマットとなっているので、時刻は空白区切りの4番目のフィールドに格納されている。 そこで、ログの行毎に4番目フィールドの時刻を epoch に変換して比較する事で指定された時間内のログを抽出できる。

192.168.15.134 - - [14/Feb/2019:03:35:54 +0900] "GET / HTTP/1.1" 200 1920 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
192.168.99.23 - - [14/Feb/2019:07:20:35 +0900] "GET / HTTP/1.1" 200 1920 "https://www.bsdhack.org/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36; 360Spider"
192.168.157.241 - - [14/Feb/2019:09:28:45 +0900] "GET / HTTP/1.1" 200 1920 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
192.168.15.143 - - [14/Feb/2019:09:58:44 +0900] "GET /bsdhack.css HTTP/1.1" 200 3246 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
192.168.93.5 - - [14/Feb/2019:12:03:47 +0900] "GET / HTTP/1.1" 200 1920 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1"
    

ただし、標準的な apache のログに格納されている時刻情報はそのままでは date (1) の '-d' オプションでは変換できない。

$ date -d "14/Feb/2019:09:28:45"
date: invalid date `14/Feb/2019:09:28:45'
    
そこで、 まずは date (1) で変換できる様な形式に変換してから変換する。 そのためには日付けの区切り文字 '/' と、日付け部と時間部を分割している ':' をスペースに置換すれば良い。
$ date -d "14 Feb 2019 09:28:45"
Thu Feb 14 09:28:45 JST 2019
    
全体の流れとしてはログファイルから1行ずつ読み込み、時刻フィールドを epoch に変換して基準時刻と比較する処理を行う。 今回は全ての処理を awk (1) を利用して実装してみる。

  1#!/bin/sh
  2#   $1: 開始時間 -- date(1) が認識できる形式
  3#   $2: 終了時間 -- date(1) が認識できる形式
  4#   $3: ログファイル
  5
  6# 開始時間
  7start=$(date '+%s' -d "${1}")
  8# 終了時間
  9end=$(date '+%s' -d "${2}")
 10# ログ抽出
 11awk -v "start=${start}" -v "end=${end}" '{
 12    # 時刻フィールドを date(1) が認識できる形式に変換
 13    gsub(/[][/]/, " ", $4)
 14    sub(/:/, " ", $4)
 15    # ログの各行について時刻を epoch に変換する
 16    # $4 にはスペースが含まれているので全体をクォートする
 17    cmd = sprintf("date +%%s -d '"'%s'"'", $4);
 18    # date(1) を実行して epoch を変数 s に取得
 19    cmd | getline s;
 20    # コマンド実行でオープンされたディスクリプタをクローズ
 21    close(cmd);
 22    # 行が範囲内なら出力する
 23    if(start <= s && end >= s)
 24        print $0
 25}' ${3}
    

これで開始時間から終了時間の間のログを抽出できる。
行毎に時間の変換処理を実行するので実行に時間はかかってしまうのが現状の問題点。

2019/01/31

とあるサイトが変更になった時になるべく確実に変更通知を受け取りたい

なるべく確実に変更通知を受け取るためには、普段携行している iPhone への通知が一番良さそうに思う。
iPhone への通知で一番最初に思い浮かぶのは PUSH通知 (APNS) なのだが、 APNS を利用するためには Apple への Developer 登録などが必要で、そう簡単に送る事ができないと思う (Developer 登録などが不要で簡単に PUSH 通知を送る方法があれば是非教えて下さい)。

そこで iPhone で受信しているアドレスにメールを送信し、 更に普段からチェックしている slack で自分宛にダイレクトメッセージを送信する事にした。
iPhone の設定によりメールを受信した時や slack でダイレクトメッセージが投稿された時の通知を有効にする事で、 変更通知がなるべく確実に受け取れる事になると思う。

そのために以下のスクリプトを Linux などが稼働しているサーバ設置して cron などにより自動実行する。

  1#!/bin/sh
  2
  3# site
  4url="https://www.example.com"
  5old=${HOME}/.old.html
  6new=${HOME}/.new.html
  7# for mail
  8addr="mail@exapmle.net"
  9subject="[SITE CHANGEED]"
 10body="SITE CHANGED: ${url}" 
 11# for slack
 12slack="https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYY/ZZZZZZZZZZZZZZZZZZZZZZZZ"
 13name="name"                 # post name
 14channel="#channel"          # post channel
 15
 16curl -s ${url} > ${new}
 17test -f ${old} &&  
 18cmp -s ${old} ${new} || echo "${body}" | mail -s "${subject}" ${addr} &&
 19curl -sLX POST --data-urlencode "payload={\"channel\": \"${channel}\", \"username\": \"${name}\", \"text\": \"${body}\"}" ${slack}
 20
 21mv $new $old
    
slack でチャンネルではなくダイレクトメッセージを送信する場合は channel の値を適切に変更する。
web を検索すると `channel: @名前` とすれば良いとの記事を多く見かけるが、少なくとも最近の slack ではエラーとなってしまう。
その場合、slack の web 画面の左側に表示されている `ダイレクトメッセージ` をクリックした時に遷移するページの URL の最後の部分 (https://XXX.slack.com/messages/YYYYY/YYYYY の部分) を指定するとダイレクトメッセージが送信できる。

2017/11/29

迷惑メール対策

最近、自宅に届く迷惑メールの量が急増しているのでドメイン単位で受信を拒否するスクリプトを作成した。
自宅メールサーバ環境では mh (1) を利用しているので、迷惑メールは spam フォルダに格納する事を想定している。
メールの自動振り分け処理も mh に付属の slocal (1) コマンドを利用しているので、 迷惑メイルの送信元ドメインを ${HOME}/.maildelivery に格納する様にしている。

  1domains=/etc/postfix/generics-domains                   # 自分のドメインを格納しているファイル
  2spamdir=$HOME/.Mail/spam                                # 迷惑メールを格納しているディレクトリ (1メール1ファイル)
  3delivery=$HOME/.maildelivery                            # maildelivery ファイル
  4tmpdir=/tmp/spam.$$                                     # 一時ファイル
  5reject="/etc/postfix/reject"                            # postfix リジェクトファイル
  6count=0
  7
  8if [ $(ls -1 ${spamdir} | wc -l) -gt 0 ]
  9then
 10    # spam ディレクトリのメールファイルから From を取得
 11    for i in $(sed -n '/^From/ s/.*<.*@\(.*\)>.*/\1/gp' ${spamdir}/* | sort | uniq)
 12    do
 13        # From が自分のドメインに詐称されていない送信ドメインを maildelivery ファイルに "destroy" として追加
 14        grep -q ${i} ${domains} || grep -q "\"@${i}\"" ${delivery} || { printf "from\t\"@%s\"\t\t\tdestroy\tA\t-\n" $i; count=$((count + 1)); }
 15    done >> ${delivery}
 16
 17    # 迷惑メールがある場合
 18    if [ ${count} -gt 0 ]
 19    then
 20        # 迷惑メールの学習
 21        mkdir -p ${tmpdir}
 22        cp -r ${spamdir} ${tmpdir}
 23        sa-learn --spam ${tmpdir}
 24        rm -r ${tmpdir}
 25    fi
 26
 27    # 迷惑メール削除
 28    rmm all +spam
 29
 30    # reject ファイル作成
 31    awk '{
 32        if($1 == "from" && $3 == "destroy" && $2 !~ "@\"$"){
 33            gsub("[\"@]", "", $2)
 34            if ($2 ~ /.+\..+/)
 35                printf "%s DISCARD\n", $2;
 36        }
 37    }' ${delivery} > ${reject}
 38    # HASH 形式に変換
 39    postmap ${reject}
 40    # 再読み込み
 41    service postfix reload
 42fi
    

メールを処理する時に迷惑メールは spam フォルダに仕分ける必要はあるが、このスクリプトを crontab に仕込んでおけば mh (1) の spam フォルダに格納されているメールの送信元ドメインからのメールは拒否出来る様になる。
reject ファイルが肥大してしまうのが難点なのだが迷惑メールは目に見えて減ったので重宝している。


Copyright © 2008-2020 Mitzyuki IMAIZUMI. All rights reserved.