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 で初期化されます。
- 実体は実行ファイル内の「データ領域」に配置されます。
メモリレイアウトとスタック
典型的なプロセスのメモリ空間はアドレスの低い方から高い方へおおよそ次のように並びます。
スタックの動作(スタックフレーム)
関数を呼び出すたびに、スタック上に「スタックフレーム」と呼ばれる領域が積まれます。中身はおおよそ次のようなものです。
- 呼び出し元に戻るためのリターンアドレス
- 呼び出し元のベースポインタの退避値(スタックフレーム同士を辿るため)
- 引数(レジスタ渡しの場合もあります)
- その関数のローカル変数
具体的なスタック利用の流れ
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}
- main() が呼ばれるとスタックに main() 用のフレームが積まれ(プッシュ)、 スタック上に変数 x、y、z が確保されます。
- 関数 add(x, y) が呼ばれるとスタックにリターンアドレスなどを積んだ上で さらに add() 用のフレームが積まれ、引数 a、b、変数 result が確保されます。
- add() が return するとそのフレームはスタックから取り除かれ(ポップ)、 制御は main() に戻ります。 add() のローカル変数 (a、b、result) はこの時点で消滅します。
- main() が return すると、main() のフレームも取り除かれます。
ダングリングポインタによる 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 種類に分類できます。- システムコールを呼び出さない純粋なライブラリ関数
- 内部でシステムコールをラップしているライブラリ
strlen(3)、 sin(3) などで、これらは文字列操作や CPU 上での計算処理のみで完結しカーネルには影響しません。
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 種類あります。- モード切り替
- プロセス切り替え
前述したカーネルモードとユーザーモードの切り替えです。
このタイプのコンテキストスイッチはシステムコール発行や割り込み・例外発生時に起こります。
同一プロセス内での切り替えであり、後述する「プロセス切り替え」よりコストは小さいですがそれなりに負荷やコストがかかります。
OS のスケジューラがプロセス/スレッドの実行を中断して別のプロセス/スレッドに実行を渡す処理です。 タイムスライスの満了、I/O 待ちによるブロック、割り込みによるプリエンプションなどで発生します。 ページテーブル (= アドレス空間) 自体が切り替わるためモード切り替えよりもさらにコストが大きくなります。
システムコールは、必ずモード切り替えを伴います。 更にそのシステムコールの処理が入出力など I/O 待ちなどでブロックが発生する場合には さらにプロセス切り替えを誘発することもあります。
コンテキストスイッチのシステム負荷・影響
モード切り替え(システムコール発行)のコスト要因
システムコールを 1 回発行するだけでも以下のような処理コストが発生します。- レジスタの退避・復元
- 特権レベルの遷移
- パイプラインフラッシュ・投機実行のリセット
呼び出し元の CPU レジスタ一式をカーネルスタックへ保存し復帰時に復元します。
CPU 内部の カーネルモードへの遷移、および復帰時のユーザーモードへの遷移で、CPU の内部的な検証・準備処理を伴います。
モード遷移命令は CPU の命令パイプラインをある程度フラッシュさせ、 投機実行による先読みの効果を失わせます。
これらの要因により、単純なシステムコール 1 回の呼び出しコストは、通常の関数呼び出しと比較して 100 から 1000 倍以上のコストがかかります。
プロセス/スレッド切り替えのコスト要因
システムコールがブロッキング I/O などで待ち状態に入ると、 OS のスケジューラは別のプロセス/スレッドを実行するためにプロセス切り替えを行いますが、 これには前述のモード切り替えのコストに加えて以下のコストが上乗せされます。- ページテーブルの切り替え
- スケジューラの実行コスト
- キャッシュの入れ替え
アドレス空間が完全に入れ替わるためTLB 全体が無効化されます。
次に実行すべきプロセス / スレッドを選択するためのスケジューラの処理が実行されます。
キャッシュに載っていた前プロセスのデータ・命令が新しいプロセスのアクセスによって徐々に追い出される事により、 キャッシュミス率が一時的に急増して実行速度が低下します。
頻繁なシステムコールがもたらすシステム全体への影響
例えばアプリケーションが 1 バイトずつ read(2) や write(2) システムコールを呼び出すような設計をすると、 以下のような問題が顕在化します。- スループットの低下
- CPU 使用率の増加
- キャッシュ効率の悪化
- スケーラビリティの低下
- レイテンシのジッター増加
システムコール自体のオーバーヘッドがボトルネックとなり本来の I/O 帯域を活かせなくなります。
ユーザー時間ではなくシステム時間として 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.gzCaps 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 $oldslack でチャンネルではなくダイレクトメッセージを送信する場合は 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 ファイルが肥大してしまうのが難点なのだが迷惑メールは目に見えて減ったので重宝している。

