構造体によく似たC言語の機能に「共用体」(union)があります。これは,IPv4をIPv6に拡張するときなど,カーネルの機能を拡張する際によく使われます。共用体の使い方とカーネルでの利用例を実際に見ていきましょう。

 カーネルは,機能がよく拡張されます。例えば,ネットワーク・プロトコルの「IPv4」と「IPv6」の関係です。現在の主流はIPv4ですが, LinuxカーネルはIPv6に対応しています。ソース・コードを見ると分かるように,IPv6のコードを一から書いているのではなく,IPv4を処理する部分でIPv6と共用できるところを機能拡張しています。ここで使うのが,「共用体」(union)です。

 共用体は,カーネル内部のあらゆる部分に利用されています。カーネルの読者にはとても大事なものですが,C言語の教科書の多くには詳細な解説がありません。そこでまず最初に,共用体とは何かを説明しましょう。

共用体(union)とは

 共用体(union)は「同一の記憶場所に複数のデータ構造を持った名前を指定できる機能」です。

 unionを使った簡単なプログラム1(図1)を見てください。3行目から7行目で共用体「ipv4」を定義しています。上から順に,


 1 #include <stdio.h>
 2
 3 union ipv4 { ←IPアドレスの共用体ipv4を定義
 4  unsigned char c[4];
 5  unsigned short s[2];
 6  unsigned int i;
 7 };
 8
 9 int main()
10  {
11   union ipv4 addr;
12   int i;
13
14   addr.c[0]=192; ←IPアドレス「192.168.1.23」をセット
15   addr.c[1]=168;
16   addr.c[2]=1;
17   addr.c[3]=23;
18   for(i=0; i<3; i++){ ←IPアドレス(192.168.1.)までを表示
19    printf("%d.", addr.c[i]);
20   }
21   printf("%d\n", addr.c[i]); ←残りの(23)を表示
22
23   printf("%04X:%04X\n",addr.s[0],addr.s[1] );
24   printf("%04X:%04X\n", htons(addr.s[0]), htons(addr.s[1]));
25
26   printf("%X\n", addr.i );
27   printf("%X\n", htonl(addr.i));
28  }
図1●共用体の例(プログラム1)


char c[4]
;short s[2]
;int i;

となっています。これは同じ記憶場所に3つのデータ型を割り当てるという意味です。3行ともにデータの長さは4バイトです。charは1バイト長なのでc [4]は4バイト,shortは2バイト長なのでs[2]も4バイトです。intは4バイト長*1ですから,iも4バイトになります。これを図にしたのが図2です。このように,同じ記憶場所を3種類のデータ型で指定できるのが共用体です。

図2●プログラム1で定義している共用体「ipv4」型
図2●プログラム1で定義している共用体「ipv4」型
4バイトの同一記憶場所を異なる3組のデータで表現する。

*1

 コンパイラによっては,int型を2バイト,あるいは4バイトで処理します。共用体は,同一の記憶場所を利用するので,利用の前に各データ型の長さを確認しておく必要があります。確認は簡単です。


#include <stdio.h>
 int main()
 {
  printf("char = %d byte\n", sizeof(char) );
  printf("short = %d byte\n", sizeof(short) );
  printf("int = %d byte\n", sizeof(int) );
  printf("long = %d byte\n", sizeof(long) );
 }

このプログラム(sizeof.c)を作成し,次のように実行します。


# cc sizeof.c

# ./a.out

char  = 1 byte
short = 2 byte
int   = 4 byte
long  = 8 byte

 すると,char型が1バイト長で,short型が2バイト長,int型が4バイト長,long型が8バイト長と確認できました。

 図1はIPアドレスを想定しています。定義した共用体ipv4の実記憶場所名「addr」を11行目で確保します。これは構造体の実記憶場所名を確保するのと同じ方法です。

 Cコンパイラはchar,short,int,long,float,doubleのデータ型を知っています。しかし,ユーザーが勝手に使用する構造体や共用体のデータ型は分かりません。そのため,使用する前にユーザーが定義して(3行目から7行目),Cコンパイラにデータ型を告知するわけです。

 一度Cコンパイラに共用体ipv4型を知らせれば,11行目のunion ipv4をchar,short,int,long,float,doubleのデータ型と同じように扱えます。ですから,12行目にあるint iでint型の実記憶場所名iを確保するように,union ipv4型の実記憶場所名addrを確保するためには11行目のようにします。

 14行目から17行目で,共用体addrのc[0]からc[3]に数値を初期設定します。すなわちIPアドレス「192.168.1.23」という値を入れます。図3のように,図示すると簡単に理解できます。このように図を描くことが大切です。

図3●共用体「addr」に値をセット
図3●共用体「addr」に値をセット

 設定した値を18行目から21行目で出力します。IPv4アドレスは,1バイトの値を4組の10進数値で示し,それらの間にドットを入れて表現します。最初の3つ(192.168.1)までを表示している部分が,


18 for(i=0; i<3; i++){
19 printf("%d.", addr.c[i]);
20 }

です。「%d.」のところで「.」を挿入しています。最後の「23」はドットを入れませんから21行目にあるように「%d」(改行されるので「\n」が付く)と処理します。

 プログラム1をコンパイルし,実行してみると,次のようになります。


# cc ipv4.c

# ./a.out

192.168.1.23
A8C0:1701
C0A8:0117
1701A8C0
C0A80117

 実行結果の「A8C0:1701」は,プログラムの23行目で出力しています。14~17行目でセットしたIPアドレスを,前半16ビット(s [0]),後半16ビット(s[1])に分けて16進数で表示しています。例えば,「A8」は10進数の「168」,C0は10進数の「192」です。c [0]からc[3]には値を設定しましたが,s[0]とs[1]には何も値を設定していません。それにもかかわらず,s[0]などの値が出力されるのは, unionで同じ記憶場所を指定しているためです。

 c[0]とc[1]の1バイトの箱にはそれぞれ,192と168を代入しました。しかし,実際にはパソコンで処理するバイト順にメモリーに記録されるので,s[1]をそのまま出力すると,「A8C0」と,C0とA8が逆になって出力されます。それを正しい順に戻して表示するのが,24行目のhtons ()関数です。

 26,27行目は,ロング型(4バイト)で出力しました。この時も,順番が入れ替わっています。それをネットワーク順に変換するためにhtonl()関数を利用します。

 以上のように,同じ記憶場所に保持されている値をchar型配列,short型配列,int型といった複数のデータ型で処理できる優れものがunionなのです。