PHPの参照をめぐる冒険

PHPの参照をめぐる冒険

知久です。

少しばかりPHPから離れてしまっていることもあって(最後にやったPHP案件はまだ数年前だけど、その1つ前はバージョン3や4の時代・・・w)、つい先日Facebook上で「Copy on write」っていうのがあって、特にPHPでは参照渡しなど使わなくても大丈夫などと教えてもらったりしたのですが、その時「やはり技術者は日々勉強だな」と反省し、少しPHPの参照周りを調べてみました。今回はちょっとその辺の報告です(間違いのご指摘、歓迎します)。

とりあえず、結論は以下のようです(説明はその後しますw)。

php_reference

そして、この結論をもう少し掘り下げてみます。

そもそも関数の引数は?

関数の呼び出しの種類として、よく聞かれる言葉に

  • Call by value(値渡し)
  • Call by reference(参照渡し)
  • Call by sharing(共有渡し)→参照の値渡しとも言われています

というものがあります。この中で「Call by reference(参照渡し)」と「Call by sharing(共有渡し)」の違いが多少難しく、Google先生に聞いても「わかっていない人が多い」という文言が目立ちますので、ここは気合いの入れどころのようですw。

じゃ、「参照渡し」ってなんだ?あたりを読んで(中の引用のところ)、僕なりに解釈すると

渡された仮引数が「左辺値」として使われる場合、
言い換えれば、「左辺値」として扱われることを意図した関数の呼び出しは
「Call by reference(参照渡し)」であり、
意味がない場合「Call by value(値渡し)」である。

という感じでしょうか。

function func($value)//←仮引数
{
    $value = うんたらかんたら;
}

という使い方をしているのが、くどいですが、「Call by reference(参照渡し)」だと解釈できます。

ちなみに「実引数は呼んでいる側の引数」で「仮引数は関数側で受ける引数」です。

もっと具体的に見ていきましょう(「渡し」という言い方が流通してしまっているので「参照による呼び出し」のような「呼び出し」を使わず、「渡し」を使っていきます)。

値渡し

下のコードを見て下さい。

<?php
function func1($value)//←仮引数
{
    $value = $value * 2;
}

function func2($value)
{
    $data = $value * 2;
    return $data;
}

$num = 123;
func1($num);//←実引数
echo $num."\n";// => 123
$num = func2($num);
echo $num."\n";// => 246

function func1($value)//←仮引数
{
    $value = $value * 2;
}

$num = 123;
func1($num);//←実引数

で呼ばれ、実引数で渡した123を2倍にして仮引数を上書していますが、その上書きが、関数の外のecho文では活かされていません。123のまま出力されています。

もちろん、func2のようにreturn文で$numを書き換えれば、$numに反映はできますが、この場合、仮引数の$valueは右辺としての意味はありますが、左辺としての意味はありません!

とうわけで、これらの関数の呼び出しでは仮引数を左辺においても意味がなく、「Call by value(値渡し)」ということになります。

参照渡し

次は前のとほんのちょっとだけ違うコードです。単に仮引数の前に「&」が付いてるのが違うだけです。

<?php
function func(&$value)//←仮引数
{
    $value = $value * 2;
}

$num = 123;
func($num);
echo $num."\n";// => 246

今度の場合は仮引数を左辺にして「値を2倍に」した結果が$numに反映されています(246が出力されています)。つまり、実引数を仮引数に渡し、これを左辺に利用して意味がある使い方になり「Call by reference(参照渡し)」ということになります。

C++やPascalなども同様のことができるようですが、Javaではできません(「Javaではできないだと〜」と思った人は次の章を読みましょうw)。PHPではこの&を仮引数に付けることにより、参照渡しが実現できるということです。

ちなみにJavaは次の「Cal by sharing(共有渡し)」であり、PHPもオブジェクトを代入した時の場合などもそうです。JavascriptやPython、Rubyなども「共有渡し」だそうなので、ここは重要です。

共有渡し

ちょっと長目ですが、次のコードを見て下さい。まずはRefTestというクラスは2つのインスタンス変数があり、コンストラクタでこれらの変数を初期化するという、とっても単純なクラスを定義しています。

通常はインスタンス変数はpublicではなく、privateでクラスを作るべきですが(そしてgetter、setterを作る)、話を単純にするために外からでもアクセスできるpublic変数にしています。

<?php
// コンストラクタとpublicな変数が2つある単純なクラス
class RefTest
{
    public $value1;
    public $value2;

    function __construct($v1, $v2)
    {
        $this->value1 = $v1;
        $this->value2 = $v2;
    }
}

このクラスをnewしていくつかの関数にそのオブジェクトを渡して、試してみましょう。

まずはref1関数。

// $refのメンバ変数value1を10に変更
function ref1($ref)
{
    $ref->value1 = 10;
}

$obj = new RefTest(1, 2);
// コンスタラクタの引数がそのまま出力
echo $obj->value1 . "\n"; // => 1
echo $obj->value2 . "\n"; // => 2

// ref1メソッドでvalue1の方が10に変更される
ref1($obj);
echo $obj->value1 . "\n"; // => 10
echo $obj->value2 . "\n"; // => 2

$obj = new RefTest(1, 2);
ref1($obj);

で、RefTestクラスをnewし、ref1関数に$objを渡します。関数の中身は

function ref1($ref)
{
    $ref->value1 = 10;
}

なので、value1の方を10に変更され、

echo $obj->value1 . "\n"; // => 10
echo $obj->value2 . "\n"; // => 2

となります。これは「Cal by sharing(共有渡し)」で、「「Cal by reference(参照渡し)」ではありません。どうして参照渡しじゃないかがポイントなんですが、次のref2関数の場合と比べてみましょう。

// 新しいオブジェクトで仮引数を上書き
function ref2($ref)
{
    $ref = new RefTest(100, 200);
}
$obj = new RefTest(1, 2);

// 仮引数に新しいRefTestオブジェクトをnewして代入←しかし、出力は前のまま
ref2($obj);
echo $obj->value1 . "\n"; // => 1
echo $obj->value2 . "\n"; // => 2

このref2の場合、関数の中で仮引数を新しくnewしたRefTestオブジェクトで上書きしています。そして、その時のコンストラクタの引数が(100,200)なのですが、関数実行後のvalue1やvalue2の出力はそのままです。

何が違うかというと下のように、仮引数である$refのメンバー変数を変更しているということと(左)、仮引数自体を書き換えていることです

$refのメンバー変数を変更している 仮引数自体を書き換えている


$ref->value1 = 10;


$ref = new RefTest(100, 200);

つまりPHPでは、変数にオブジェクトが格納されているという条件下では、明示的に&を仮引数に付けずに普通に関数を呼び出した場合、オブジェクトのリファレンスが実引数からコピーされて仮引数に渡り、別々の変数が同じオブジェクトを見ているのですが、仮引数を新しいオブジェクトで関数内で書き換えると、別々のオブジェクトを指すようになります。図にすると下のような感じでしょうか(上の2つの図)。

ref2

これは、結局アドレスという値がコピーされて仮引数に渡されるので「参照の値渡し」とも呼ばれ、人によっては「値渡し」という人もいますが、オブジェクトのメンバ等にはアクセスして、変更等もできるので、若干「値渡し」とは挙動が違いますので、ここでは共有渡しと呼んでおきます。

参照渡しと比べておきましょう。

// 新しいオブジェクトで仮引数を上書き(ただし参照渡し!)
function ref3(&$ref)
{
    $ref = new RefTest(100, 200);
}
// 参照渡しの仮引数を新しいRefTestオブジェクトをnewして代入←出力が変わる
ref3($obj);
echo $obj->value1 . "\n"; // => 100
echo $obj->value2 . "\n";// => 200

繰り返しですが、「function ref3(&$ref)」のように「&」が付いているだけで、アドレスがコピーされるのではなく、実質同じ変数になります(仮引数が実引数のエイリアスになるイメージでしょうか?変数のアドレス自体が渡るという言い方をする人もいます)。したがって、仮引数を新しいオブジェクトで関数内で書き換えると、どちらの変数も同じ新しいオブジェクトを指すようになります。

「実質」と言っているのは、内部的にPHPがどのように処理しているかは深入りしないということですw(「php zval」というキーワードで検索すると良いかもしれません)。

「値渡し/参照渡し/共有渡し」のまとめ

ここでまとめておきます。PHPでは、次のようになっています。

  • オブジェクトを入れた変数以外は「値渡し」
  • オブジェクトを入れた変数は「共有渡し」
  • 関数の仮引数に&を入れた場合は「参照渡し」

ということになります。以下、3つで違うことを頭に入れる必要がありそうですねw

共有渡しでも、オブジェクトの中身(メンバ変数)を変えることが可能 共有渡しでは、仮引数自体を書き換えても意味がない(左辺になれない) 参照渡しだと、左辺になれる(つまり実引数の方も変更される)
function ref1($ref)
{
    $ref->value1 = 10;
}
function ref2($ref)
{
    $ref = new RefTest(100, 200);
}
function ref3(&$ref)
{
    $ref = new RefTest(100, 200);
}
Copy-On-Write

さて、話がさらに複雑になります。上記でまとめた1番の項目オブジェクトを入れた変数以外は「値渡し」ですが、その値の内容を変えずに、参照だけをするだけなら「共有渡し」的に渡すというのが「Copy-On-Write」の考え方です。例えば、次のような大きな配列に値を入れた場合、値渡しするとメモリーがもったいないよね(大きな配列が2つになるので)、という発想です。

次のようなコードを実行すると、①のところをコメントアウトしていると普通に実行できますが、コメントを取るとメモリー不足のエラーが起こります。

<?php
#メソッド1
# 100万個の配列の値をすべて2倍にする
function func($arr)
{
    $end = count($arr);
    for ($i = 0; $i < $end; $i++) {
        // $arr[$i] = $arr[$i] * 2; // ①
    }
    return $arr;
}
# ここからMainメソッド
$array = Array();
# 100万個の配列作成
for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

$array = func($array);

つまり、私のMacBook Airでは、デフォルトを倍にしたphp.ini内の「memory_limit」が

memory_limit = 256M

で①が実行されなければ、エラーもなく実行できるけれど、①が実行されると

Allowed memory size of 268435456 bytes exhausted

というエラーが起こる、ということです。

どうしてかというと、

        $arr[$i] = $arr[$i] * 2; // ①

では、配列に入っている値を2倍にして再格納しているので、渡された配列の中身を変えています。つまり渡された変数が変更されない限り、共有渡し的にになるわけですが、今回の例では実際には変更しているので、

この

$array = func($array);

で「func」というメソッドが呼ばれた時配列自体がコピーされて、100万個の配列が2個でき、メモリー不足になったわけです。「Copy On Write」はまさに「書きこむ時にはコピーする」ということなわけです。

ただ、今回何度も出てきた

function func(array &$arr)

&$arrの「&」をつけることにより、参照渡しになり、配列が2つできないので(コピーされないので)、先ほどの条件でもメモリー不足にはなりません。

とはいえ、「Copy On Write」の登場により、今回の例のように渡した配列の中身を変更したりしない限り、「&」を付けなくても大丈夫ということになります

このことによって、あまり「参照渡し」を利用する機会がなくなってきている(あるいはむしろバグの温床にもなる)というようなことを言っている人もいて、概ね、私も賛成です。ただ積極的に参照渡しにしないと困るという状況以外では、利用しない方が良いと思うのですが、一方で、配列の中身を変更する場合はコピーされるということでもあり、メモリー不足になってエラーになってしまう場合もあり得るということも事実です。

そもそも100万個の配列を扱うこと自体ダメじゃん、ということは言えますし、512Mではエラーにならないので、そうすることも可能ですが、それでも状況によっては、やはり、どうしても色々な理由で—予算の関係でメモリーを増やせないとか、配列をいくつかに分解して処理すると遅くなってしまうなど—100万個の配列を扱いたいというようなケースも現実にはあり、その場合は参照渡しを検討すべきかな、と思うわけです。

おまけ(どっちが速い?)

最後に参照渡しと値渡しでどれくらい速度が違うんだろうかな、と実験してみました。これは、与えられた条件で違ってくるものなので、PHPの関数では、参照渡しよりもreturnしたほうが速い!?では、どでかい文字列を参照変数に代入するのはパフォーマンス的に遅いというようなことを言っています。が、このリンク先の例では、「参照渡し」するような積極的な意義はなさそうです(先ほど、メモリー不足のような状況が起こるケースでは、と言いましたが、ここの例では空文字の変数を渡しており、単にリターン文で書き換えればもともと良い、というようなケースに見えますw)。

<?php
// reference.php
function func(array &$arr)
{
  $end = count($arr);
  for ($i = 0; $i < $end; $i++) {
        $arr[$i] = $arr[$i] * 2;
    }
}

for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

func($array);

というような参照を渡した場合の結果が下のよう。

$ php reference.php 

real    0m0.481s
user    0m0.381s
sys     0m0.098s

また、値渡しの場合の下のようなコードでは

// value.php
function func(array $arr)
{
  $end = count($arr);
  for ($i = 0; $i < $end; $i++) {
        $arr[$i] = $arr[$i] * 2;
    }
    return $arr;
}

for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

$array = func($array);

実行結果は、少しだけ、参照を渡したときよりも遅いですね。

time php value.php 

real    0m0.567s
user    0m0.449s
sys     0m0.116s

関数の値渡しと参照渡しどちらが速い?でも、似たような結果になっていますw

今回はこれでおしまい。

『メールサーバを立てる』–まとめ–

せっかくメールサーバを構築したので、後々のためにブログに残しておくことにしました。

基本的な流れを追って、シリアスな(?)ところは省いたりしています。この設定でも十分実用的だとは思うのですが。

メールサーバを構築する

メールサーバの構成を決める

メールサーバをどのような形で守っていこうかを決めておきましょう。
特に、スパムメールの対処法とパスワード認証ですかね。全体的な構成を以下のようにします。

  • SpamAssassin(スパムアサシン)でスパムフィルタリング。
  • そのために、Procmailが必要。
  • IMAP / POP3 サーバは、Dovecot(ダヴコット)を使い、IMAPに設定します。
  • SMTPサーバは、Postfixです。
  • SMTPの認証は、saslauthdでプレインテキストまたはmd5ログイン認証とします。

必要なものをインストール

# yum install postfix dovecot spamassassin
# yum install cyrus-sasl cyrus-sasl-md5 cyrus-sasl-plain

※ Procmail はおそらくインストール済みと思います。

設定の大まかな流れ

dovecot 設定手順
  1. 各設定ファイルを編集する
    • /etc/dovecot/dovecot.conf
    • /etc/dovecot/conf.d/10-auth.conf
    • /etc/dovecot/conf.d/10-mail.conf
    • /etc/dovecot/conf.d/10-master.conf
    • /etc/dovecot/conf.d/10-ssl.conf
  2. サービス実行
    • systemctl start dovecot.service
  3. システム起動時にサービスを自動実行
    • systemctl enable dovecot.service
spamassasin 設定手順
  1. SpamAssassin用のユーザの作成
  2. SpamAssassin設定ファイル最新化スクリプトの作成
    • /etc/cron.daily/spamassassin-update
  3. SpamAssassin学習スクリプト作成
    • /etc/cron.hourly/spamassassin-learn
  4. procmail 設定ファイル作成
    • /etc/procmailrc
    • /etc/logrotate.d/procmail
  5. スパム専用メールボックス作成
    • 既存ユーザ用
    • 新規ユーザ用
postfix 設定手順
  1. Postfix 設定ファイル編集
    • /etc/postfix/main.cf
    • /etc/postfix/master.cf
  2. sendmail をやめて、postfix に切り替える

  3. postfix、saslauthd、spamassassinの実行およびシステム起動時の自動実行

設定の詳細

各設定ファイルの内容を中心に記録しておくことにします。

dovecot 設定

以前は一つのファイルで設定していたと思うのですが、設定内容ごとにファイルに分けられています。

/etc/dovecot/dovecot.conf
  # 24行: コメントを外し変更(複数記述できますが、そのポートが開きます)
  protocols = imap
  # 30行: IPv6を使わないときは、コメントを外し変更する
  listen = *
/etc/dovecot/conf.d/10-auth.conf
  # 10行: プレインテキスト認証の時には、コメントを外して変更する
  disable_plaintext_auth = no
  # 100行: 追加
  auth_mechanisms = plain login
/etc/dovecot/conf.d/10-mail.conf
  # 30行: コメントを外して追加
  mail_location = maildir:~/Maildir
/etc/dovecot/conf.d/10-master.conf
  # 96-98行: コメントを外して追加
  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
      mode = 0666
      user = postfix # 追加
      group = postfix # 追加
  }
/etc/dovecot/conf.d/10-ssl.conf
  # 8行: SSLを使わないので変更
  ssl = no
spamassasin 設定
SpamAssassin用のユーザの作成
 # groupadd spamd
 # useradd -g spamd -s /bin/false -d /var/log/spamassassin spamd
 # chown spamd:spamd /var/log/spamassassin
SpamAssassin設定ファイル最新化スクリプトの作成と実行
  • spamassassin-update を作成する。

    #!/bin/bash
    # SpamAssassin設定ファイル最新版ダウンロード
    cd /etc/mail/spamassassin
    wget -qN http://www.flcl.org/~yoh/user_prefs
    
    # 設定ファイル更新時のみSpamAssassin再起動
    diff user_prefs user_prefs.org > /dev/null 2>&1
    if [ $? -ne 0 ]; then
    cp user_prefs local.cf
    
    # スパム判断したメールを添付形式にしないように設定
    echo "report_safe 0" >> local.cf
    
    #ベイジアンフィルタを有効にする最低のham数(defaultは200)
    echo "bayes_min_ham_num 1" >> local.cf
    
    #ベイジアンフィルタを有効にする最低のspam数(defaultは200)
    echo "bayes_min_spam_num 1" >> local.cf
    
    # スパム判断したメールの件名に「***SPAM***」を付加するように設定
    echo "rewrite_header Subject ***SPAM***" >> local.cf
    
    # SpamAssassin再起動
    /etc/rc.d/init.d/spamassassin restart > /dev/null
    fi
    cp user_prefs user_prefs.org
  • SpamAssassin設定ファイルを最新にする。

    # chmod +x spamassassin-update
    # ./spamassassin-update
  • 毎日自動実行させる。

    # mv spamassassin-update /etc/cron.daily/
SpamAssassin学習スクリプト作成
  • spamassassin-learn

    #!/bin/bash
    PATH=/usr/sbin:/usr/bin:/bin
    
    for user in `ls /home/`
    do
    # 正常メール
    hammail=/home/$user/Maildir/cur
    
    # 正常メール学習
    if [ -d "$hammail" ]; then
        # 正常メールをSpamAssassinに学習させる
        su $user -s "/bin/bash" -c "sa-learn --ham $hammail | \
        logger -p mail.info -t 'sa-learn for $user'"
    fi
    
    # スパムメール
    spammail=/home/$user/Maildir/.Spam/cur
    
    # スパムメール学習
    if [ -d "$spammail" ]; then
       # スパムメールをSpamAssassinに学習させる
       su $user -s "/bin/bash" -c "sa-learn --spam $spammail | \
       logger -p mail.info -t 'sa-learn for $user'"
    
       # 受信後一ヶ月経過したスパムメールを削除
       tmpwatch -m 720 $spammail
    fi
    done
  • スクリプトを1時間ごとに実行

    # chmod +x spamassassin-learn
    # mv spamassassin-learn /etc/cron.hourly/
procmail 設定ファイル作成
  • /etc/procmailrc

    SHELL=/bin/bash
    PATH=/usr/bin:/bin
    DROPPRIVS=yes
    MAILDIR=$HOME/Maildir
    DEFAULT=$MAILDIR/
    SPAM=$MAILDIR/.Spam/
    LOGFILE=$MAILDIR/.procmail.log # ログ出力先
    #VERBOSE=ON # 詳細ログ出力
    
    # SpamAssassinによるスパムチェック
    :0fw
    |/usr/bin/spamc
    
    # SpamAssassinがスパム判定したメールはスパム専用メールボックスへ配送
    :0
    *^X-Spam-Flag: YES
    $SPAM
  • procmailログローテーション設定ファイル作成
    /etc/logrotate.d/procmail
    /home/*/Maildir/.procmail.log {
    missingok
    nocreate
    notifempty
    }
スパム専用メールボックス作成
  • 既存ユーザ用
    スパム専用メールボックス作成スクリプトを作成し実行する
    spamfolder-create

    #!/bin/bash
    
    for user in `ls /home`
    do
    id -u $user > /dev/null 2>&1
    if [ $? -eq 0 ] && [ ! -d /home/$user/Maildir/.Spam/new ]; then
        mkdir -p /home/$user/Maildir/.Spam/new
        mkdir -p /home/$user/Maildir/.Spam/cur
        mkdir -p /home/$user/Maildir/.Spam/tmp
        chmod -R 700 /home/$user/Maildir/.Spam
        chown -R $user. /home/$user/Maildir/.Spam
        echo $user
    fi
    done
  • 新規ユーザ用
    新規ユーザ追加時に自動でスパム専用メールボックス作成

    # mkdir -p /etc/skel/Maildir/.Spam/{new,cur,tmp}
    # chmod -R 700 /etc/skel/Maildir/.Spam
postfix 設定

全体をまとめるような感じで設定していきます。

Postfix 設定ファイル編集
  • /etc/postfix/main.cf
    コメント行が多いので、有効にした行と有効になっていた行を集めてみました。
    行頭がスペースで始まると、上の行の続きになります。コピーするときには注意してください。
queue_directory = /var/spool/postfix
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix
mail_owner = postfix
mydomain = 【メールサーバにするドメイン】# example.com
myorigin = $mydomain
inet_interfaces = all
inet_protocols = all
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
unknown_local_recipient_reject_code = 550
mynetworks_style = subnet
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
home_mailbox = Maildir/
mail_spool_directory = /var/mail
mailbox_command = /usr/bin/procmail
debug_peer_level = 2
debugger_command =
     PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
     ddd $daemon_directory/$process_name $process_id & sleep 5
sendmail_path = /usr/sbin/sendmail.postfix
newaliases_path = /usr/bin/newaliases.postfix
mailq_path = /usr/bin/mailq.postfix
newaliases_path = /usr/bin/newaliases.postfix
mailq_path = /usr/bin/mailq.postfix
setgid_group = postdrop
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /usr/share/doc/postfix-2.10.2/samples
readme_directory = /usr/share/doc/postfix-2.10.2/README_FILES

 ##################################################
 # ここから下は追加
 #

smtpd_client_restrictions =
  permit_mynetworks,
  reject_unknown_client,
  permit

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth

smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous

smtpd_recipient_restrictions =
  permit_mynetworks,
  permit_auth_destination,
  permit_sasl_authenticated,
  # reject_non_fqdn_recipient,
  # reject_unauth_destination,
  reject

mailbox_size_limit = 1000000000

message_size_limit = 20000000

smtpd_recipient_limit = 50
  • /etc/postfix/master.cf
    該当行のコメントを外して、編集します。

    smtp      inet  n       -       n       -       -       smtpd
      -o content_filter=spamassassin
    submission inet n       -       n       -       -       smtpd
      #  -o smtpd_tls_security_level=encrypt
      -o smtpd_sasl_auth_enable=yes
      -o smtpd_client_restrictions=permit_sasl_authenticated,reject

    最後に付け加えます。

    spamassassin unix - n n - - pipe flags=R user=spamd argv=/usr/bin/spamc
      -e /usr/sbin/sendmail
      -oi -f ${sender} ${recipient}
sendmail をやめて、postfix に切り替える
 # systemctl stop sendmail.service
 # systemctl disable sendmail.service
 # alternatives --config mta
postfix、saslauthd、spamassassinの実行およびシステム起動時の自動実行
 # systemctl start postfix.service
 # systemctl enable postfix.service
 # systemctl start saslauthd.service
 # systemctl enable saslauthd.service
 # systemctl start spamassassin.service
 # systemctl enable spamassassin.service

以上で終了です。

/etc/init.d/ にinitスクリプトがない!

systemd とサービスの起動・停止

友人がサーバ機にFedora19をインストール。頼まれて、メールサーバを構築することになったのですが、/etc/init.d/ に起動・停止スクリプトがない!

私自身はFedora12以降システム系をいじっていなかったので、すっかり時代遅れになっていました。Fedora15からは、デフォルトでSystemV風ではなくなりsystemdが採用されているんですね。というわけですっかり浦島太郎な私は、脱rc.dしなければなりません。

systemctl の使い方

ユニットを使う

ユニットには、例えば、サービス (.service) やマウントポイント (.mount)、デバイス (.device)、ソケット (.socket) などがあります。

コマンドsystemctlを使うとき、一般的には拡張子 (suffix) を含むユニットファイルの完全な名前を指定する必要があります。例えば、sshd.socketのようにです。しかし、サービスユニットの場合には、拡張子を省略することができます。httpd.serviceの代わりにhttpdと記述することができます。

ユニットの確認

実行中のユニットを一覧する

$ systemctl

あるいは

$ systemctl list-units

もしもserviceのみ表示したい時には

$ systemctl list-units --type=service

インストールされたユニットを一覧する

$ systemctl list-unit-files
サービス(ユニット)ごとの状態確認
$ systemctl status [service_name].service
サービス(ユニット)のスタート・ストップ等
# systemctl start [service_name].service

# systemctl stop [service_name].service

# systemctl restart [service_name].service

# systemctl reload [service_name].service

そのほかのコマンドとして、try-restartreload-or-restartreload-or-try-restart等があります(もちろんサービスユニット以外のユニット用のコマンドも多数あります)。

サービス(ユニット)の登録
# systemctl enable [service_name].service
サービス(ユニット)の停止
# systemctl disable [service_name].service

具体例

httpdサーバをインストールしてみることにしましょう。

# yum install httpd

すると
/lib/systemd/system/httpd.service が作成されます。
ファイルの中身を見てみると、

# less /usr/lib/systemd/system/httpd.service

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=notify
EnvironmentFile=/etc/sysconfig/httpd
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful
ExecStop=/usr/sbin/httpd $OPTIONS -k graceful-stop

# We want systemd to give httpd some time to finish gracefully, but still want
# it to kill httpd after TimeoutStopSec if something went wrong during the
# graceful stop. Normally, Systemd sends SIGTERM signal right after the
# ExecStop, which would kill httpd. We are sending useless SIGCONT here to give
# httpd time to finish.
KillSignal=SIGCONT
PrivateTmp=true

[Install]
WantedBy=multi-user.target

となっています。一つだけ忘れないようにしておきたいと思うのは、サービスに [Install] セクションがない場合、大抵は他のサービスから自動的に起動される、ということでしょうか。

httpdを実行させるには、

# systemctl start httpd.service

とします。サービスの状態を確認するには、

# systemctl list-units --type=service

で、表示されるサービスの一覧の中に、

httpd.service          loaded active running The Apache HTTP Server

を見つけることができます。

サービスを止めるのは、

# systemctl stop httpd.service

です。

サービスの状態を表示(動いているかどうかなど)

$ systemctl status httpd.service

有効化(起動時に自動で実行するよう設定)されているかどうか表示

$ systemctl is-enabled httpd.service

サービスを手動ではなく、システム起動時に自動で開始する

# systemctl enable httpd.service

システム起動時に実行されないように無効化する

# systemctl disable httpd.service

とりあえずはこれでシステムのサービスについてはOKでしょう。

Facebook「いいね!」の「コメント入力欄」のポップアップ

Facebook「いいね!」ボタンは、Facebook公式プラグインを使用していますが、使い方によってWordPressのページが壊されてしまっていました。

指摘してくださった方、ありがとうございます。

2013-11-18 追加

今日、Facebook プラグインの「Like Button」を再々度(何度やったかは忘れましたが)試してみました。

すると、選択できるボタン3種類のうち、
[standard] を選択すると、相変わらずiPhoneでの表示が崩れてしまいますが、
[button_count]と[box_count] の2つではiPhoneでの表示が(一時崩れはしますが)正常になることを確認しました。

(もしかしてバグだった?のを修正?)

プラグインでiPhoneにも対応できるのなら、下の記事は不要になりますね。

無事解決しましたので、お礼がてらブログとして記録しておくことにいたしました。

「いいね!」ボタンを押すと

「いいね!」ボタンを押すと

【問題点】

写真のように、右下にある[閉じる]ボタンがきちんと表示されていません。
設定によっては、ボタンが全く表示されないため閉じることができなくなってしまいます。

これは、固定ページや投稿ページの本文中に入れる「いいね!」ボタンを押すと、コメントを入力するポップアップが出てきますが、その横幅の問題です。

横幅をデフォルトのままにすると、iPhoneで見たときにページのデザインが崩れてしまいます(これは、TwitterBootstrapとの絡みもあります)。そのために、横幅を調節したのですが…

横幅が狭いと、「いいね!」ボタンを押すと出てくるコメント入力欄の右下にある[閉じる]ボタンが見えず、コメント入力欄が開きっぱなしになってしまいます(写真の状態)。

【解決方針】

コメント入力欄が出てこないようにする。そのためには、・・・

Facebook プラグインのコードバージョンには、HTML5、XFBML、IFRAME、URL といった種類があります。

これらのうち、コメント欄のポップアップが出てくるのは、
HTML5、XFBML
の二つでした(URL版は一瞬出てきて何故かすぐ消えました)。

IFRAME 版のプラグインを使えば、コメント欄のポップアップは出てきません。

【IFRAME 版のプラグインを選択する方法】

  1. Facebook プラグインの設定項目のうち「Like Button」を選択し、「Like Button Settings」のページへ行きます。
    Facebook [Like Button]
  2. 「Read more… 」と書いてあるリンク先へ飛びます。
    Facebook [readme]
  3. 「URL to Like」の欄に、いいねをしてもらいたいページのアドレスを記入して、レイアウトなどを決めたら、下にある[Get Code]ボタンをクリックします。
    Facebook[Get Code]
  4. 現れたポップアップ内の[IFRAME]ボタンを押します。
    Facebook[IFRAME]
  5. 表示されたコードをコピーして、「いいね!」ボタンを置きたいページの場所に、コードを貼り付けます。
    Facebook[Paste Code]

おわり。

「パスワード認証はやめましょうね」の巻

「パスワード認証はやめましょうね」の巻

外に開いているサーバでsshのポートを開けていると、「/var/log/secure」のログを見ればわかるように、色々な人がsshでログインしようとしていることが伺えます。

ログインができたら、もうそれはサーバーを乗っ取られたということに等しいわけで、危険です。もしかしたらパスワードをランダムでログインしようとして、いつかは成功してしまうかもしれないからです。しかし、秘密鍵やら公開鍵を作った方が良いだろうけれど、面倒なのでパスワード認証でやっている、というサーバもありそうです(今時あまりないでしょうが、開発用の試験サーバのような暫定的な場合は結構ありそうです)。↓はsecureログの例(3秒に1回ぐらいアクセスしている・・・)。

pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=112.65.244.xxx  user=root

そこで、今回は「ほうら、sshの公開鍵方式はこんなに簡単ですよ」という話をしてみます(iptablesでアクセスできるipを絞るというようなことも可能ですが、ここでは省略)。とりあえず環境的には、ローカルはMacBook Air(Marvericks)、サーバはCentOS6.2(アドレスは「www.yyy.org」)です。

ざっくり、やることは4つだけ
  1. ローカルで公開鍵と秘密鍵を作成する(1コマンド)。
  2. サーバに公開鍵を持っていく(ログインしたいユーザの「~/.ssh/authorized_keys」に追加します(新規の場合は作成します)。
  3. サーバのsshdの「パスワード認証を使えないように」設定を変えて、sshdをリスタートする。
  4. ログインしてみる。

これで終了です。さて、具体的に見ていきます。

ローカル(Mac)でRSA鍵の作成
  1. SSH2では(もう通常はSSH2でしょう)DSAとRSAが使えますが、とりあえずRSAの公開鍵・秘密鍵を作成します。
ssh-keygen -t rsa

をMacターミナル上で上記を実行します。すると、

Enter file in which to save the key (/Users/xxx/.ssh/id_rsa):

と聞いてきます。すでにある場合は上書きされちゃうので変更する必要がありますが(一応上書きするよって聞いてくるので、心配ありません)、初めてなら、そのままリターン。

Enter passphrase (empty for no passphrase):
Enter same passphrase again:

で2度パスフレーズを入れたら、出来上がり。

Your identification has been saved in /Users/xxx/.ssh/id_rsa.
Your public key has been saved in /Users/xxx/.ssh/id_rsa.pub.

が作成されます。上が秘密鍵、下が公開鍵です。

ローカルでは、一応念のため「.ssh/config」に(なかったら新規にファイルを作って)

IdentityFile ~/.ssh/id_rsa

を書き込んでおきます。

サーバに公開鍵を置く

公開鍵なので、あまり神経質になる必要はありませんが、サーバにscpあたりで「id_rsa.pub」を転送しましょう。

scp ~/.ssh/id_rsa.pub xxx@www.yyy.org:/home/xxx

そして、とりあえず今回まではサーバにパスワード認証で入ります。

そして、↑でホームディレクトリに「id_rsa.pub」を転送したので、これを~/.ssh/authorized_keysに追記します(初めてなら新規に作成、今回は初めてなので新規に作ります)。

mkdir .ssh
mv id_rsa.pub .ssh/authorized_keys
chmod 600  .ssh/authorized_keys

そして、ローカルにある「id_rsa.pub」は通常いらないので、削除してしまいましょう。

sshdの設定変更

sshdの設定ファイルは「/etc/ssh/sshd_config」なので、これを修正します。

※「PasswordAuthentication no」は、鍵を使ってログインできることを確認してから、変更した方が無難かもしれません

Protocol 2
PermitRootLogin no #念のため
RSAAuthentication yes
PubkeyAuthentication yes
PermitEmptyPasswords no
PasswordAuthentication no

とします。そしてsshdをリスタート

/etc/init.d/sshd restart
ログイン確認
ssh xxx@www.yyy.org

でログインしようとすると、Macでは下のようなポップアップが出てきます(初めて見たときは驚きます!)。

sshmac

最初に鍵を作ったときのパスフレーズを入力すると、ログインできます。

そして、次からはパスフレーズを聞かれなくなります。

※これだとノートパソコンをどこかに忘れたりすると危険ということもあり、上記の鍵方式に加え、やはり「iptables」でIPアドレスを絞る方が心配ないですね(その分出かけたときに仕事ができませんがw)。

蛇足

この鍵を使ってのsshにすると、コマンドラインでやっている人以外、このままだとscp(WinSCPなど)が使えません。

FileZillaやWinSCPはPuTTY形式では利用できるので、問題なく使えます(FileZillaはFileZilla自身でコンバートしてくれますが、WinSCPは自分で変更する必要がありますので、とりあえずPuTTYをインストールする必要があります)。下のURL等を参考にして下さい。

終わり。

ちく

SoundflowerとQuickTimeで、ドットインストールのまねごと

MacのQuitckTimeとSoundflowerを使って、ドットインストールのような動画を作成する

QuickTimeは動画は撮れるのですが、音声を取り込めません。ですがSoundflowerというソフトを利用すると、QuickTimeで音声を取り込めるようになります(説明の中の図が小さくて見づらい場合は、図をクリックすると大きな画像が見えます)。

Soundflowerのインストール

Soundflowerのダウンロード一覧からダウンロードして、インストールします。私は一番上の新しい1.6.6bをダウンロードしましたが、動いています(最初はMountain Lion、その後Mavericksにしましたが、現状大丈夫そうですw)。

※フリーソフトですので自己責任でお願いします。

Soundflowerの設定

Soundflowerをインストールすると、下のような花がLaunchpadに現れます。

soundflower-icon

これを使って起動すると、Macの画面上上部に以下のようなアイコンが現れます(画面上部にあるアイコンバーの一番左です—小さくて見づらいですが)。

soundflower-icon2

このアイコンをクリックすると、以下のような設定画面が現れます。

soundflower-setting

この中のSoundflower(2ch)「None(off)」を2つ下の「Built-in Output」に変更します。

Macのサウンドの設定

次に、「システム環境設定」→「サウンド」で、「出力」タブ「Soundflower(2ch)」を選択します。

soundsetting

これだけでセッティングは終了です。

実際の撮影

まずはQuickTimeをLaunchpadから立ち上げます(下の図)。

quicktime

立ち上がったら、上のメニューの「ファイル」→「新画面収録」を実行します。

quicktime1

実行すると、下の図のようなコントローラが現れます。

quicktime2

このコントローラの右の方にある「下矢印」をクリックして、録音するデバイスを選択します。私の場合はUSBにつないだヘッドセットのマイクを利用するので、UAB-350を選択します。

quicktime3

そして、音声のスライダーが最低になっているので、ある程度大きくします。あとは真ん中の録画ボタンをクリックします。すると、次のようなメッセージが出ますので、今回は画面全体をキャプチャしないので、範囲をドラッグで指定します。

quicktime4

あとは真ん中に「収録を開始」ボタンがあるので、これをクリックすると録画が始まります。

quicktime5

収録が終わったら、画面上部のアイコンで○の中に■がある(下の図では右から3つ目のアイコン)をクリックすると終わります。

quicktime6
その後、お好みのファイル形式を選んで書き出せば終了です(今回は720を選んで書き出しました)。もちろん、iMovieでタイトルをつけたり、編集することも可能です。

quicktime7

実際の作品

というわけで、今回のデモ用に「ドットインストールもどき」を作成してみました。ただ、もちろん本家に比べれば、めっちゃ話が下手ですし、収録した事務所の隣のビルが現在解体中で、そのクレーンの騒音を拾っていますが、あしからず(今回は書き出した後にflvにコンバートしています)。

ちく