2026/09/02
C 言語の可変引数マクロ
C 言語の debug で画面表示やデバッグログ出力などで、 関数にするとオーバーヘッドがかかってしまう場合によく利用されるのが #define によるマクロ定義です。
1#define debug fprintf(stderr, "debug");
この様に定義する事でコンパイル時(正確にはコンパイル時に実行されるプリプロセッサにより)に
debug は fprintf(... に置き換えられます。
1#define debug fprintf(stderr, "debug") 2 : 3 : 4debug;この方法では fprintf(3) には情報を渡せませんが C 言語のマクロ定義は関数の形式で定義すると引数を渡す事が可能です。
1#define debug(_arg) fprintf(stderr, _arg) 2 : 3 : 4debug("error")この様に定義する事でマクロ debug を経由して fprintf(3) に引数を渡すことができます。 ただし、引数の数は固定なので frpintf(stderr, "error: %d", errno) の様な処理は出来ません。 そこで便利なのが可変の引数を受け取りマクロを定義する __VA_ARGS__ です。 この __VA_ARGS__ を指定する事でマクロに可変引数を指定する事ができます。
1#define debug(...) fprintf(stderr, __VA_ARGS__) 2 : 3 : 4debug("error\n"); 5debug("%s %n\n", name, errno);指定方法はマクロ定義の仮引数リストの可変部分に省略記号 (...) を指定し、 置換される文字列部分では __VA_ARGS__ を指定するだけです。 省略記号の ... は本当にドット . を 3 個つなげて書きます。
1#ifdef DEBUG \ 2#define debug(...) \ 3{ \ 4 FILE *_fp; \ 5 \ 6 if((_fp = fopen("ログファイル", "a")){ \ 7 fprintf(_fp, __VA_ARGS__); \ 8 fclose(_fp); \ 9 } \ 10} 11#else 12#define debug(...) 13#endifこの様なマクロ定義をしておくことでコンパイル時に DEBUG が定義された場合 (コンパイル時に -D DEBUG が指定された場合)でログファイルへの書き込みが可能な場合は ログファイル にログを出力させる事ができます。
マクロは 1 行で記述する必要があるので改行は \ でエスケープしています。 もちろん C 言語の文法的には改行なしで書いても全然問題ないですが、 解読性のために改行を追加しています)。
__VA_ARGS__ は 1999 年に制定されたC言語の国際標準規格 ISO/IEC 9899:1999 (C99) で公式に導入されたので現在では完全に合法な記法で安心して利用できます。 ANSI X3.159-1989 (C89) までは __VA_ARGS__ が導入されていなかったので 引数の数に応じて複数のマクロを定義したりする必要がありました (それならちゃんと可変引数の関数定義したほうが良いですよね...)。
1#define debug1(_arg1) fprintf(stderr, _arg1); 2#define debug2(_arg1, _arg2) fprintf(stderr, _arg1, _arg2); 3 : 4 :
2026/09/01
C言語のビット演算
はじめに
かつてメモリーが非常に高価な時代に 1 ビットで表現できる 「オン」か「オフ」などの状態はビットによる状態保持がほぼ常識となっていました。 プロセッサにビット演算の機能が搭載されていて ビット演算が非常に高速だった事も普及に拍車をかけていたと思います。 しかし、メモリを含むコンピュータリソースが安価になり、 プロセッサの処理性能が高速になった今の時代に敢えてビットで状態を保持する意味はあまりないと思いますが、 ビット演算について学ぶのは良いことだと思います。
ビット演算
ビット演算はデータをビット単位で操作するための演算方法です。 他にもいくつか種類がありますがここでは C 言語に演算子が用意されているビット演算を紹介します。
ビット演算子
- 論理積
-
論理積は AND と呼ばれる演算です。 2 つのビットで双方が 1 の場合のみ 1 となります。
特定のビットの状態を検査する時に多く利用されます。C 言語の演算子は & で示されます。
11 & 1 = 1 20 & 1 = 0 31 & 0 = 0 40 & 0 = 0
- 論理和
-
論理和は OR と呼ばれる演算です。 2 つのビットのどちらかが 1 の場合に 1 となります。
特定のビットを 1 にする場合に多く利用されます。C言語の演算子は | で示されます。
11 | 1 = 1 21 | 0 = 1 30 | 1 = 1 40 | 0 = 0
- 排他的論理和
-
排他的論理和は XOR と呼ばれる演算です。 2つのビットが異なる場合に 1 となります。
ビットの違いを検出する場合に多く利用されます。C言語の演算子は ^ で示されます。
11 ^ 1 = 0 21 ^ 0 = 1 30 ^ 1 = 1 40 ^ 0 = 0
- 否定
-
反転演算子は NOT と呼ばれる演算です。 ビットの状態を反転させます。 1 なら0、0なら1となります。
C言語の演算子は ~ で示されます。
1~ 1 = 0 2~ 0 = 1
- シフト
-
ビット列を左右に移動させます。 左シフトは指定された数だけビット列を左に移動、 右シフトは指定された数だけビット列を右に移動します。 移動してできたスペースには 0 か 1 が挿入されます。 2 のべき乗の乗算、除算に多く利用されます。
C言語の演算子は << 、>> で示されます。
11 << 2 = 4 21 << 4 = 16 34 >> 2 = 1 416 >> 4 = 1
ビットマスク
ビットマスクは、ビット演算を使って特定のビットを操作するための方法です。 ビットマスクを使用することで、データの特定のビットを効率的に抽出、設定、 またはクリアすることができます。
実際の演算
実際に C 言語で行うビット演算のスニペットです。
ここでは 8 ビットの変数 flags に対してのビット操作を例として示します。
変数 flags の右から 2 番目のビットを 1 にしたい場合。
右から 2 番目のビットは 2 進数で 0000 0010 と表現されます。 これは 16 進数では 0x02 と表現できるのでビットマスクは 0x02 となります。 ビットを 1 にするには論理和 (|) を取ればいいので以下のスニペットになります。
1/* 変数 flags の右から 2 番目のビットを 1 にする */ 2flags = flags | 0x02;C 言語では演算した結果を変数に戻す場合は 演算子 + = という記法が利用できるので 以下の様に |= と短く記述できます。
また 0x02 のような定数 (多くの場合マジックナンバーと呼ばれます) をプログラム中に直接書くのは有害な事とみなされ (considered harmful) ていますので、 同じく C 言語のマクロ機能で名称を付与するのが良い方法といえるでしょう。
1#define MASK 0x02 2 3flags |= MASK;元の変数 flags の値がどの様な値であっても 右から 2 番目のビットが 1 になる以外の影響はありません。 右から 2 番目のビットが元々 1 の場合は何も変化はありません。
変数 flags の右から2番目のビットが 1 か確認したい場合。
ビットが 1 かを確認するためには論理積 (&) を取ればいいのでビットマスクと論理積を取ります。 ビットマスクは右から 2 番目のビットだけが 1 なので 変数 flags の右から2番目のビットが 1の場合は 1 となります。 他のビットはビットマスクが 0 なので 0 となります。
1/* 変数 flags の右から2番目のビットが 1 か確認する */ 2if((flags & MASK) != 0) { 3 /* 右から 2 番目のビットは 1 */ 4}C言語の if 文は言語仕様として 0 か 0 以外かの判定なので短く書くこともできます。
1/* 変数 flags の左から 2 番目のビットが 1 か確認する */ 2if(flags & MASK) { 3 /* 左から 2 番目のビットは 1 */ 4}
変数 flags の右から 2 番目のビットを 0 にしたい場合。
ビットを 0 にする場合は少しだけ複雑になります。 他のビットに影響を与えずに右から 2 番目のビットを 0 にする場合は 0000 0010 の反転した値 1111 1101 と論理積を取ることで実現できます。
| 変数 flags の値 | 1010 1010 |
| ビットマスクを反転した値 | 1111 1101 |
| 論理積の結果 | 1010 1000 |
1/* 変数 flags の右から 2 番目のビットを 0 にする */ 2flags &= ~MASK;
シフト演算
左シフト演算は乗算に利用できます。 左に 1 シフトすると溢れがなければ元の値が 2 倍になります。 一番右側には 0 が挿入されます。右シフト演算は除算に利用できます。 右に 1 シフトすると元の値が 1/2 になります。 一番左側は符号付きの場合は符号桁が継承され(算術シフト)、 符号なしの場合は 0 が挿入され(論理シフト)ます。 かつてはプロセッサの乗算、除算能力が極端に低い場合があったので、 2 のべき乗の演算にはよく利用されましたが、桁溢れの処理が煩雑になる事やコードが直感的ではない事、 プロセッサの性能向上などにより最近では推奨されない使い方になっているようです。 現在では後述の可搬性のあるビットマスク生成のために利用される事が多いです。
変数 i と j の値を入れ替える。
以前は高速でメモリ使用量を極小にしたまま変数の値を入れ替える事ができると言われていたアルゴリズムです。 2 つの変数に排他的論理和を 3 度繰り返す事で他の変数を使用する事なく値を入れ替える事ができます。
このアルゴリズムは i 、j において (i XOR j) XOR j = i が成立する 排他的論理和の対称差という性質を利用したものです。
現代では却って遅くなる場合があると言われていますし、 浮動小数点型の変数では利用できません。 そもそも処理内容が見た瞬間には判りずらいので推奨されていません。
1int i, 2 j; 3 4i ^= j; 5j ^= i; 6i ^= j;ちなみに値が十分に小さい整数の場合は以下の四則演算でも同様の結果となりますが、 値が大きくなると算術オーバーフローが発生するおそれがあるのでこちらも強く非推奨です。
1int i, 2 j 3 4i = i + j; /* i += j */ 5j = i - j; 6i = i - j; /* i -= j */
ビットマスクについて
上の例では変数 flags を 8 ビットと仮定して C 言語のコードを記載しましたが、
C 言語では変数の実際のサイズは厳密に定められていません。
例えば char 型の変数は最低 8 ビット必要、
int 型の変数は最低 16 ビット必要としか規格では定まっていません
(現在世の中にある C 言語の処理系で char が 8 ビットではない、
int が 32 ビットでない処理系は多分存在しないと思いますが...)。
ただし最近の 64 ビット CPU では int が 32 ビットではなく 64 ビットとなっている場合があるので要注意です。
ですので int 型変数の一番左のビットを操作するためのビットマスクとして 32 ビットを仮定して 0x80000000 を指定すると想定とは異なった挙動となってしまう可能性があります。
1#define MASK 0x80000000 2 3unsigned long flags; 4 5/* 変数 flags の一番左のビットを 1 にする */ 6flags |= MASK;この様に処理系依存の数値でも正しく動作するためには、 ヘッダーファイル limits.h で定義されているマクロを利用します。 たとえば unsigned long 型の変数に格納できる最大の数値は マクロ ULONG_MAX に格納されているので、 unsigned long の変数に対するビットマスクでは以下の方法で指定するのが可搬性があり安全な方法です。
1#include <limits.h> 2 3#define MASK ~(ULONG_MAX >> 1) 4 5unsigned long flags; 6 7/* 変数 flags の一番左のビットを1にする */ 8flags |= MASK;ちなみにC99 (ISO/IEC 9899:1999) では変数については以下の項目しか規定されていません。
- 整数として表現できる範囲の大きさは以下の関係
- singed char の大きさは 8 ビット以上
- short int の大きさは 16 ビット以上
- int の大きさは 16 ビット以上
- long int の大きさは 32 ビット以上
- long long int の大きさは 64 ビット以上
singed char ≦ short int ≦ int ≦ long int ≦ long long int
未定義なので鼻から悪魔を出しても(nasal demons) 仕様には反していません。
2026/08/27
Linux ネットワークプログラミング
Linux 上で動作するネットワーク関係の C プログラムのスニペットです。
一部のコードは Linux 固有の機能を使っていますので残念ながら Linux 上でしか動作しません。
一応の動作確認は実施していますが、処理の概要を示しただけなのでエラー処理等は省いています。
実際に製品に組み込む場合はエラーチェック処理は必須です。
指定されたインタフェイスの情報を取得する
オープンされた L3 層の UDP ソケット (ネットワーク層) に対して ioctl(2) を発行する事で各種情報をカーネルから取得する事ができます。 ここでは例として以下の情報を取得して表示するコードを示します。
- アドレス
- ネットマスク
- MAC アドレス
1/* ioctl(2) 用に socket を作成 */ 2if((sock = socket(AF_INET, SOCK_DGRAM, 0)) >= 0){ 3 const char *ifname = "eth0"; 4 struct ifreq ifr; 5 6 /* ioctl(2) 用にインタフェイス名をセット */ 7 memset(&ifr, 0, sizeof(ifr)); 8 strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); 9 10 /* ioctl(2) を発行して IP アドレスを取得 */ 11 if(ioctl(sock, SIOCGIFADDR, &ifr) >= 0){ 12 struct sockaddr_in *src_ip_addr = (struct sockaddr_in *)&ifr.ifr_addr; 13 printf("IP Address: %s\n", inet_ntoa(src_ip_addr->sin_addr)); 14 } 15 16 /* ioctl(2) を発行してネットマスクを取得 */ 17 if(ioctl(sock, SIOCGIFNETMASK, &ifr) >= 0){ 18 struct sockaddr_in *netmask_addr = (struct sockaddr_in *)&ifr.ifr_netmask; 19 printf("Netmask: %s\n", inet_ntoa(netmask_addr->sin_addr)); 20 } 21 /* ioctl(2) を発行して MAC アドレスを取得 */ 22 if(ioctl(sock, SIOCGIFHWADDR, &ifr) >= 0){ 23 unsigned char *mac = (unsigned char *)ifr.ifr_hwaddr.sa_data; 24 printf("MAC Address: %02x:%02x:%02x:%02x:%02x:%02x\n", 25 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); 26 } 27}
指定されたインタフェイスの対向アドレスを取得する
指定されたインタフェイスが Peer to Peer の場合はローカルと対向の IPv4 アドレスを取得します。 指定されたインタフェイスが Peer to Peer 以外の場合はどちらも同じ値となります。 L3 層の DGRAM ソケット (UDP ソケット) を使用しているので通常のユーザにも実行可能です。
1if((sock = socket(AF_INET, SOCK_DGRAM, 0)) >= 0){ 2 const char *ifname = "eth0"; 3 struct ifreq ifr; 4 5 /* ioctl(2) 用にインタフェイス名をセット */ 6 memset(&ifr, 0, sizeof(ifr)); 7 strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); 8 9 /* ioctl(2) を発行して自分自身のアドレスを取得 */ 10 if(ioctl(sock, SIOCGIFADDR, &ifr) >= 0){ 11 struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_dstaddr; 12 } 13 14 /* ioctl(2) を発行して対向のアドレスを取得 */ 15 if(ioctl(sock, SIOCGIFDSTADDR, &ifr) >= 0){ 16 struct sockaddr_in *sin2 = (struct sockaddr_in *)&ifr.ifr_dstaddr; 17 } 18}
指定されたインタフェイスと通信可能なアドレスの一覧取得
こちらは Linux 特有の NetLink という仕組みを利用して 指定されたインタフェイスの ARP テーブルを参照して通信可能な IPv4 アドレスを取得します。 サンプルではチェック処理などを省いていますが、 実際には環境によって大量のアドレスが取得できるので受け取り側のオーバーフローには要注意です。
1const char *ifname = "eth0"; 2unsigned int index = if_nametoindex(ifname); 3 4if(index > 0){ 5 /* Netlink ソケットを開く */ 6 if((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) >= 0){ 7 8 /* リクエストメッセージの組み立て (RTM_GETNEIGH = ARPテーブル取得) */ 9 memset(&req, 0, sizeof(req)); 10 req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ndmsg)); 11 req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; 12 req.nlh.nlmsg_type = RTM_GETNEIGH; 13 req.ndm.ndm_family = AF_INET; /* IPv4を指定 */ 14 15 /* カーネルに問い合わせ */ 16 if(send(sock, &req, req.nlh.nlmsg_len, 0) >= 0){ 17 /* カーネルからの応答を受信・パース */ 18 while((status = recv(sock, buff, sizeof(buff), 0)) > 0){ 19 struct nlmsghdr *nlh; 20 21 for(nlh=(struct nlmsghdr *)buff; NLMSG_OK(nlh, status); nlh=NLMSG_NEXT(nlh, status)){ 22 /* メッセージ終了(DONE)ならループを抜ける */ 23 if((nlh->nlmsg_type == NLMSG_DONE) || (nlh->nlmsg_type == NLMSG_ERROR) 24 break; 25 26 struct ndmsg *ndm = (struct ndmsg *)NLMSG_DATA(nlh); 27 /* 対象のインターフェースインデックスと一致しない場合や 28 * 完全に死んでいるもの (NUD_FAILED や NUD_INCOMPLETE) は除外する 29 */ 30 if((ndm->ndm_ifindex != index) || (ndm->ndm_state & (NUD_INCOMPLETE | NUD_FAILED))) 31 continue; 32 33 /* 属性(IPアドレスやMACアドレス)をパース */ 34 struct rtattr *rta = (struct rtattr *)((char *)ndm + NLMSG_ALIGN(sizeof(struct ndmsg))); 35 int rta_len; 36 37 for(rta_len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct ndmsg)); 38 RTA_OK(rta, rta_len); 39 rta = RTA_NEXT(rta, rta_len)) 40 /* NDA_DST が宛先IPアドレス(ARPテーブルのIP)を示す */ 41 if(rta->rta_type == NDA_DST) 42 ipaddr[i++] = *(unsigned long *)RTA_DATA(rta); 43 } 44 } 45 } 46 close(sock); 47 }
IPアドレスとネットマスクを元にユニキャストアドレスを抽出
IPv4アドレスの一覧からブロードキャストアドレスやネットマスクなどの情報を除外して ユニキャストアドレスだけを抽出する処理です。
1unsigned long target_ip, 2 my_ip, 3 netmask; 4 5/* 255.255.255.255 もしくは 0.0.0.0 は除外 */ 6if(target_ip == 0xffffffff || target_ip == 0) 7 return 0; 8 9/* マルチキャストアドレス (224.0.0.0 - 239.255.255.255) を除外 */ 10/* target_ip の第 1 オクテットをチェック */ 11unsigned char *ip_bytes = (unsigned char *)&target_ip; 12 13if(ip_bytes[0] >= 224 && ip_bytes[0] <= 239) 14 return 0; 15 16unsigned long network_addr = my_ip & netmask, 17 broadcast_addr = my_ip | ~netmask; 18 19/* ネットマスクもしくはブロードキャストアドレスを除外 */ 20if(target_ip == network_addr || target_ip == broadcast_addr) 21 return 0; 22else if(ip_bytes[3] == 255 || ip_bytes[3] == 0) 23 return 0;
IPv4アドレスがネットワークに属しているか確認する
1const char *net, 2 *mask, 3 *addr; 4 5if(inet_pton(AF_INET, network, &net) == 1 && 6 inet_pton(AF_INET, netmask, &mask) == 1 && 7 inet_pton(AF_INET, ipaddr, &addr) == 1){ 8 9 // ネットワークアドレス側はすでにマスク済みの想定であれば、左辺のマスクは省略可能です 10 return(addr.s_addr & mask.s_addr) == net.s_addr; 11}
CIDR 形式にも対応させる
1const char *net, 2 *mask 3 *addr; 4 5if(prefix >= 0 && prefix <= 32){ 6 if(inet_pton(AF_INET, network, &net) == 1 && 7 inet_pton(AF_INET, ipaddr, &addr) == 1){ 8 /* プレフィックス長からホストバイトオーダーでマスクを作成して 9 * ネットワークバイトオーダー(ビッグエンディアン)に変換 10 */ 11 uint32_t mask = htonl((prefix == 0) ? 0 : (~0U << (32 - prefix))); 12 13 return(addr.s_addr & mask) == (net.s_addr & mask); 14 } 15}
複数回実行される場合の高速化
ループ内など複数回実行される場合は文字や変換処理など inet_pton(3) がボトルネックになってしまうので、 あらかじめネットワークアドレスとネットマスクを数値データ (uinit_32) に変換して保持しておいた方が高速化できます。
1/* ネットワーク情報を保持しておく構造体 */ 2typedef struct{ 3 uint32_t net; 4 uint32_t mask; 5} rule; 6 7/* あらかじめ IPv4 アドレスも unit32 に変換する */ 8uinit_32 addr; 9 10/* 判定処理は数値演算のみ */ 11return(addr & rule->mask) == rule->net;
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 となる。

