タグ別アーカイブ: C/C++

MySQL 5.1.41 + 勉強

mysqld (MySQL 5.1.41) ソースコードざっくり勉強メモ。
関数の呼び出し順番とかを主に。そして適当に。
動作環境は CentOS 5.4。

オプション解析

my.cnf とか。

sql/mysqld.cc       main():4301
  init_common_variables() が出発点
 
sql/mysqld.cc       init_common_variables():3286
  load_defaults()
 
mysys/default.c     load_defaults():383
  my_load_defaults()
 
mysys/default.c     my_load_defaults():427
  my_search_option_files() 通過後、argc, argv を読み込んで、値を整形。
 
mysys/default.c     my_search_option_files():153
  my.cnf を fgets() してる
 
sql/mysqld.cc       init_common_variables():3289
  get_options()
 
sql/mysqld.cc       get_options():8495
  handle_options()
 
mysys/my_getopt.c   handle_options():112
  オプション構造体や変数に設定値を格納してる

ネットワーク初期化

listen socket 作成とか。

sql/mysqld.cc   main():4403
  network_init()
 
sql/mysqld.cc   network_init():1619
  socket(), bind(), listen() してる。

接続受付

accept とか。

sql/mysqld.cc   main():4517
  handle_connections_sockets() を呼んでる。
 
sql/mysqld.cc   handle_connections_sockets():4993
  select(), accept() してる。
  成功したら create_new_thread() を呼んでる。

受信

パケット受信とか。

sql/mysqld.cc   create_new_thread():4918
  accept 後など、コネクションに対して新しくスレッドを作成するとき呼ばれると思われる関数。
  いろいろ設定後、scheduler_functions::add_connection() を呼んでる。
  add_connection() は関数ポインタで create_thread_to_handle_connection() がセットされてる。
 
sql/mysqld.cc   create_thread_to_handle_connection():4853
  実際にスレッドを作成してる関数。
  特に何事もなければ handle_one_connection() をコールバック関数にしてスレッドを作成してる。
 
sql/sql_connect.cc  handle_one_connection():1074
  1コネクションに対するスレッドのハンドラ。
  スレッド初期化とかパケット受信とかコマンド発行とかしてる。
  1080行目で、scheduler_functions::init_new_connection_thread() を呼んでる。
  init_new_connection_thread() は関数ポインタで init_new_connection_handler_thread() がセットされてる。
 
sql/sql_connect.cc  init_new_connection_handler_thread:611
  1コネクションに対するスレッドの初期化処理が行われる関数と思われる。
  618行目で、my_thread_init()  を呼んでる。

pthread_key + 使い方

pthread_key 周辺の使い方を勉強。
スレッドごとにメモリ領域を確保する仕組み。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
static pthread_key_t g_key;
 
// スレッド単位で保持するデータ
struct MyData{
    int count;
    char data;
};
 
static void* thread_call( void *arg )
{
    pthread_t self_id = pthread_self();
    printf( "self_id:%u\n", self_id );
 
    // データがすでに存在するか調査
    struct MyData *p;
    if( pthread_getspecific( g_key ) ){
        printf( "self_id:%u Already exists.", self_id );
        return NULL;
    }
 
    // データ作成
    if( !(p = (struct MyData *)calloc( 1, sizeof(*p) )) ){
        perror( "calloc()" );
        return NULL;
    }
    pthread_setspecific( g_key, p );
    p->count = 0;
    p->data = 'A';
 
    // 本当にスレッド単位で保持されているかテスト
    int i;
    for( i = 0; i < 26; ++i ){
        p = (struct MyData *)(pthread_getspecific( g_key ));
        printf( "self_id:%u id:%u data:%c\n", self_id, p->count++, p->data++ );
        usleep(1000);
    }
    p = (struct MyData *)(pthread_getspecific( g_key ));
    if( p->count == 26 ){
        printf( "self_id:%u success.\n", self_id );
    }
    else{
        printf( "self_id:%u error!!\n", self_id );
    }
 
    // 終了
    free(p);
    pthread_setspecific( g_key, 0 );
}
 
int main()
{
    // pthread key を初期化
    if( pthread_key_create( &g_key, NULL ) ){
        perror( "pthread_key_create()" );
        return 0;
    }
 
    // スレッドを複数作成。
    const int TNUM = 3;
    pthread_t tid[TNUM];
    int i;
    for( i = 0; i < TNUM; ++i ){
        if( pthread_create( &tid[i], NULL, thread_call, NULL ) ){
            perror( "pthread_create()" );
            continue;
        }
        printf( "Created a thread. id:%u\n", tid[i] );
    }
 
    // 全スレッドが終了するのを待機。
    for( i = 0; i < TNUM; ++i ){
        pthread_join( tid[i], NULL );
    }
 
    // pthread key を削除
    pthread_key_delete( g_key );    
    return 0;
}

実行

$ gcc main.c -lpthread
$ ./a.out
Created a thread. id:663552
Created a thread. id:2625536
Created a thread. id:3162112
self_id:663552
self_id:2625536
self_id:3162112
self_id:663552 id:0 data:A
self_id:2625536 id:0 data:A
self_id:3162112 id:0 data:A
self_id:663552 id:1 data:B
self_id:2625536 id:1 data:B
...
self_id:2625536 id:25 data:Z
self_id:663552 success.
self_id:3162112 id:25 data:Z
self_id:2625536 success.
self_id:3162112 success.

send と recv と送信バッファ溢れ

send と recv の使い方と送信バッファ溢れについて。

manpage

Manpage of SEND
Manpage of RECV

SEND

non-blocking の場合。
send が失敗して外部変数 errno に EINTR が設定されている場合は、再度送信を試みる必要がある。
データをソケットの送信バッファに入れることが出来ない場合、外部変数 errno に EAGAIN か EWOULDBLOCK が設定される。
いつデータが送信できるようになるかを知るために select などのシステムコールを使うことが出来る。(後述:送信バッファ溢れ)

以下、ソースコードから抜粋した send のサンプル。

int res = 0;
char* p = buffer; // 送るデータ
size_t len = buffer_size; // 送るデータのサイズ
while( len > 0 ){
    while( true ){
        res = send( socket, p, len, MSG_DONTWAIT ); // 一度に全部送れるとは限らない
        if( errno != EINTR ) break; // システム割り込みチェック
    }
    if( res < 0 ){
        if( errno == EAGAIN ||
            errno == EWOULDBLOCK ){
            // 書き込めない状態。それなりの対応が必要(後述:送信バッファ溢れ)
        }
        else{
            // なんらかのエラー(コネクション切断とか)
        }
    }
    len -= res;
    p += res;
}

RECV

non-blocking の場合。
操作が停止するような場合、外部変数 errno に EAGAIN か EWOULDBLOCK が設定される。

以下、ソースコードから抜粋した recv のサンプル。

int res = 0;
while( true ){
    res = recv( socket, buffer, buffer_size, MSG_DONTWAIT );
    if( errno != EINTR ) break;
}
if( res == 0 ){
    // EOF
}
 
if( res < 0 ){
    if( errno == EAGAIN ||
        errno == EWOULDBLOCK ){
        // not ready yet.
    }
    else{
        // error.
    }
}

送信バッファ溢れ

データ送信時に送信バッファが溢れてしまった場合、書き込み可能なタイミングを見計らって、溢れたデータを再送信してあげる必要がある。
送信バッファが溢れたとき、send 関数は失敗し、errno に EAGAIN, EWOULDBLOCK を格納する。
対策として例えば、送信データをキューなり可変長バッファなりで一時的に保持し、select などで書き込み可能になるタイミングを監視して、再送信する方法がある。

サンプル:sendbuffer-overflow
事前に libevent をインストールしておく必要あり。
動作環境 Linux のみ。(送信バッファと受信バッファの数値を linux/sockios.h 使って確認してる為)
1.サーバは受信時に3秒間待機する。
2.クライアントはサーバへデータを送信しまくり、EAGAIN or EWOULDBLOCK を発生させる。
3.エラー発生後、クライアントは書き込み可能イベントを監視し、検知したらデータを再送信する。
2ー3の動作を繰り返し、サーバに規定のデータ(0 – 499 までの連番)が全部届いたら成功。

インストール

$ git clone http://github.com/utahta/sendbuffer-overflow.git
$ cd sendbuffer-overflow
$ ./configure --prefix=/path/to/sendbuffer-overflow --with-libevent=/path/to/libevent
$ make
$ make install

実行

コマンドプロンプトをふたつ立ち上げ、1つめにサーバを起動します。

$ /path/to/sendbuffer-overflow/bin/sbt_server

2つめにクライアントを起動します。

$ /path/to/sendbuffer-overflow/bin/sbt_client

手元では、以下のような結果になりました。

$ /path/to/sendbuffer-overflow/bin/sbt_server
connected. sd:7
recv buffer size:274 sd:7
waiting 3 sec.
... 省略
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:495
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:496
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:497
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:498
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:499 // 499 まで届いた
$ /path/to/sendbuffer-overflow/bin/sbt_client
send buffer size. :50436
... 省略
send buffer size:48498 res:0 sd:6
send buffer size:48772 res:0 sd:6
send buffer size:49046 res:0 sd:6
not ready yet: Resource temporarily unavailable // 書き込み失敗
send buffer size:49152 res:-10 sd:6
not ready yet: Resource temporarily unavailable
send buffer size:49152 res:-10 sd:6
... 省略
resend buffer size:88558 res:0 sd:6 // 書き込み可能になったので再送信

このサンプルでは、可変長処理をさぼってあらかじめ大きなバッファを確保してごまかしてたり。
終了処理に手抜きがあったり。(delete してなかったり)
とりあえずサンプルということで。

TCPサーバ + サンプル

TCPサーバのサンプルコードのメモ。
簡単なコードだからか、何度も書いてはどこかへやってたのでいい加減保存することに。

動作は一応 Linux CentOS5, Mac OS X 10.6 で確認済み。

server/main.cpp に socket, bind, listen, accept, select, recv など。
client/main.cpp に socket, connect, send など。

ソースコード

simple-tcpserver

インストール

$ git clone http://github.com/utahta/simple-tcpserver.git
$ cd simple-tcpserver
$ ./configure --prefix=/path/to/simple-tcpserver
$ make
$ make install

実行

・サーバ

$ cd /path/to/simple-tcpserver/bin
$ ./easy_tcpserver

・クライアント

$ cd /path/to/simple-tcpserver/bin
$ ./easy_tcpclient

MySQL + GDB + 解析

MySQL 5.1.41 を GDB 使って解析するメモ。取っかかり編。
環境 : CentOS 5.2

ダウンロード

MySQL 5.1.41 のソースをダウンロードする。

インストール

/usr/local/mysql-5.1.41 へインストールする。

$ tar zxvf mysql-5.1.41.tar.gz
$ cd mysql-5.1.41
$ ./configure --prefix=/usr/local/mysql-5.1.41 --with-unix-socket-path=/usr/local/mysql-5.1.41/tmp/mysql.sock --with-charset=utf8 --with-extra-charsets=all --enable-thread-safe-client --with-plugins=innodb_plugin --with-readline --with-debug
$ make
$ make install
$ cd /usr/local/mysql-5.1.41
$ sudo cp share/mysql/my-medium.cnf /etc/my.cnf

GDB

最初に mysqld を gdb 経由で実行する。

$ gdb ./libexec/mysqld
GNU gdb Red Hat Linux (6.5-25.el5rh)
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1".

立ち上がったら続けて、main に break ポイントを打ってみる。

(gdb) b main
Breakpoint 1 at 0x81b697a: file mysqld.cc, line 4261.

main は sql/mysqld.cc の 4261 行目にある。
とりあえず実行。

(gdb) r
Starting program: /usr/local/mysql-5.1.41/libexec/mysqld
[Thread debugging using libthread_db enabled]
[New Thread -1209051440 (LWP 10008)]
[Switching to Thread -1209051440 (LWP 10008)]
 
Breakpoint 1, main (argc=1, argv=0xbf96e304) at mysqld.cc:4261
4261      MY_INIT(argv[0]);             // init my_sys library & pthreads

MY_INIT(argv[0]) のとこで止まった。(ちなみに MY_INIT() はマクロで、include/my_sys.h の 40 行目にある)
いま止まってるとこ付近のソースを表示する。

(gdb) l
4256    int win_main(int argc, char **argv)
4257    #else
4258    int main(int argc, char **argv)
4259    #endif
4260    {
4261      MY_INIT(argv[0]);             // init my_sys library & pthreads
4262      /* nothing should come before this line ^^^ */
4263
4264      /* Set signal used to kill MySQL */
4265    #if defined(SIGUSR2)

main の直後 に MY_INIT() が呼ばれており、#else 〜 #endif で windows とそれ以外で main を切り分けてるのがなんとなく分かる。
ステップ実行して MY_INIT() に入る。

(gdb) s
my_init () at my_init.c:73
73        if (my_init_done)
Current language:  auto; currently c

my_init() へ辿り着いた。my_init() は MY_INIT() の中で呼ばれており、mysys/my_init.c の 73 行目にある。
my_init_done の値を確認する。

(gdb) p my_init_done
$1 = 0 '\0'

0 が入ってる。my_init() 実行前だから当然っちゃ当然。(my_init_done は my_bool型(typedef char))

やっぱりステップ実行は便利ですね。

参考

GDB マニュアル