欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

mysqld_safe启动脚本源码阅读、分析

程序员文章站 2024-03-01 19:11:10
前几天读了下mysqld_safe脚本,个人感觉还是收获蛮大的,其中细致的交代了mysql数据库的启动流程,包括查找mysql相关目录,解析配置文件以及最后如何调用mysq...

前几天读了下mysqld_safe脚本,个人感觉还是收获蛮大的,其中细致的交代了mysql数据库的启动流程,包括查找mysql相关目录,解析配置文件以及最后如何调用mysqld程序来启动实例等,有着不错的参考价值;与此同时,脚本中涉及了很多shell编程中的小技巧,像变量解析,sed替换转义,进程优先级的判断以及无处不在test结构等等,当作linux shell的学习素材还是非常合适的,下面是我的环境:

数据库版本: mysql 5.1.45
操作系统版本: red hat enterprise linux as release 4 (nahant update 3)
mysql基目录: /usr/local/mysql3306
配置文件目录: /usr/local/mysql3306/etc

数据库是安装好了的,代码如下:

#!/bin/sh

# 一些状态变量的定义
kill_mysqld=1;  # 试图kill多余的mysqld_safe程序,1表示需要kill
mysqld=      # mysqld二进制可执行文件的名称
niceness=0    # 进程的调度优先级标识

# 下面的变量主要用于标识不使用错误日志和syslog
logging=init   # 日志记录状态,init代表初始化
want_syslog=0   # 标识是否要使用syslog
syslog_tag=
user='mysql'   # --user选项值
pid_file=     # pid文件的路径
err_log=     # 错误日志的路径

# 这两个都是定义的syslog中标志位,在后面需要写入日志到syslog中时使用
syslog_tag_mysqld=mysqld
syslog_tag_mysqld_safe=mysqld_safe

trap '' 1 2 3 15			# 不允许程序在终端上被人打断(包括挂起,中断,退出,系统终止的情形)

umask 007       # 默认权限770,其他组用户对该程序创建的文件没有任何权限

# defaults变量记载使用的配置文件的信息
defaults=
case "$1" in
  --no-defaults|--defaults-file=*|--defaults-extra-file=*)
   defaults="$1"; shift
   ;;
esac

# usage()函数:使用--help选项时输出的使用帮助信息
usage () {
    cat <<eof
usage: $0 [options]
 --no-defaults       don't read the system defaults file
 --defaults-file=file    use the specified defaults file
 --defaults-extra-file=file also use defaults from the specified file
 --ledir=directory     look for mysqld in the specified directory
 --open-files-limit=limit  limit the number of open files
 --core-file-size=limit   limit core files to the specified size
 --timezone=tz       set the system timezone
 --mysqld=file       use the specified file as mysqld
 --mysqld-version=version  use "mysqld-version" as mysqld
 --nice=nice        set the scheduling priority of mysqld
 --skip-kill-mysqld     don't try to kill stray mysqld processes
 --syslog          log messages to syslog with 'logger'
 --skip-syslog       log messages to error log (default)
 --syslog-tag=tag      pass -t "mysqld-tag" to 'logger'

all other options are passed to the mysqld program.

eof
    exit 1
}

# my_which的作用相当于which,通过检索$path中的路径,打印出命令的全路径
# 这个函数就在后面一个地方用到了,就是my_which logger,意思等同于转换logger为/usr/bin/logger
my_which ()
{
 save_ifs="${ifs-unset}"   # 保存当前的内建分隔符,用于后面重置ifs
 ifs=:            # 使用 : 来分割path中的路径
 ret=0
 for file           # 这种写法等同于for file in &*
 do
  for dir in $path
  do
   if [ -f "$dir/$file" ]
   then
    echo "$dir/$file"
    continue 2       # continue 第 2 层, 这里就是跳出外层循环了
   fi
  done
	ret=1 # signal an error
	break
 done

# 将设置过的ifs重置回去
 if [ "$save_ifs" = unset ]
 then
  unset ifs
 else
  ifs="$save_ifs"
 fi

 return $ret # success
}

# 日志输出函数,这是个原型,后面被log_error和log_notice函数引用
log_generic () {
 # priority 代表日志信息的分类,从后面的两个函数可知有:daemon.error和daemon.notice两种类别
 priority="$1" 
 shift

# 日志中记录的msg前缀格式: 时间 + mysqld_safe ,类似于系统日志的记录格式
 msg="`date +'%y%m%d %h:%m:%s'` mysqld_safe $*"
 echo "$msg"
 case $logging in
  init) ;;                # 初始化状态时,只在命令行输出msg信息,不记录日志
  file) echo "$msg" >> "$err_log" ;;   # 记录到err_log中
  syslog) logger -t "$syslog_tag_mysqld_safe" -p "$priority" "$*" ;; # 使用logger记录到系统日志中
  *)
   echo "internal program error (non-fatal):" \
      " unknown logging method '$logging'" >&2
   ;;
 esac
}

# 下面两个函数是对log_generic函数中不同分类的引用
log_error () {
 log_generic daemon.error "$@" >&2
}

log_notice () {
 log_generic daemon.notice "$@"
}

# 后面就是用它启动的mysqld,通过logging变量区分记录日志的类型,分错误日志和系统日志syslog两种
# 最后的eval命令会解析 $cmd 中的值并执行命令
eval_log_error () {
 cmd="$1"
 case $logging in
  file) cmd="$cmd >> "`shell_quote_string "$err_log"`" 2>&1" ;;
  syslog)
   cmd="$cmd 2>&1 | logger -t '$syslog_tag_mysqld' -p daemon.error"
   ;;
  *)
   echo "internal program error (non-fatal):" \
      " unknown logging method '$logging'" >&2
   ;;
 esac
 #echo "running mysqld: [$cmd]"
 eval "$cmd"
}

# 转义函数,用于在非"a-z","a-z","09",'/','_','.','=','-'的特殊字符前加上一个"\"
# sed中的\1代表引用前面\(\)中匹配的值
shell_quote_string() {
 echo "$1" | sed -e 's,\([^a-za-z0-9/_.=-]\),\\\1,g'
}

# 该函数用于解析配置文件中的选项,并赋值给相应的变量
parse_arguments() {
 pick_args=
 if test "$1" = pick-args-from-argv
 then
  pick_args=1
  shift
 fi

 for arg do
  # 取出参数值,比如 --port=3306 结果为: val = 3306 注意这里sed中使用;来分割,等同于/
  val=`echo "$arg" | sed -e "s;--[^=]*=;;"`
  case "$arg" in
   # 将参数值传递给对应的变量
   --basedir=*) my_basedir_version="$val" ;;
   --datadir=*) datadir="$val" ;;
   --pid-file=*) pid_file="$val" ;;
   --user=*) user="$val"; set_user=1 ;;

   # 有些值可能已经在my.cnf配置文件的[mysqld_safe]组下设置了
   # 某些值会被命令行上指定的选项值覆盖
   --log-error=*) err_log="$val" ;;
   --port=*) mysql_tcp_port="$val" ;;
   --socket=*) mysql_unix_port="$val" ;;

   # 接下来这几个特殊的选项在配置文件的[mysqld_safe]组中是必须设置的
   # 我没配置这个组,所以就用不到了(使用mysqld中的默认)
   --core-file-size=*) core_file_size="$val" ;;
   --ledir=*) ledir="$val" ;;
   --mysqld=*) mysqld="$val" ;;
   --mysqld-version=*)
    if test -n "$val"
    then
     mysqld="mysqld-$val"
    else
     mysqld="mysqld"
    fi
    ;;
   --nice=*) niceness="$val" ;;
   --open-files-limit=*) open_files="$val" ;;
   --skip-kill-mysqld*) kill_mysqld=0 ;;
   --syslog) want_syslog=1 ;;
   --skip-syslog) want_syslog=0 ;;
   --syslog-tag=*) syslog_tag="$val" ;;
   --timezone=*) tz="$val"; export tz; ;; # 生效了一下时区设置

   --help) usage ;; # 调用了usage函数,输出帮助信息

   *)
    if test -n "$pick_args"
    then
     # 将其他命令行参数值附加到$arg的后面
     append_arg_to_args "$arg" 
    fi
    ;;
  esac
 done
}

########################################
# 正式工作开始了!!
########################################

#
# 下面两段是在寻找基目录和mysqld所在目录
#
# 找到/usr/local/mysql3306/share/mysql目录,使用relpkgdata来记录相对路径和绝对路径
# 这个grep其实应该是想判断一下share/mysql是不是显示的绝对路径,不知道这么写的意义在哪里。
if echo '/usr/local/mysql3306/share/mysql' | grep '^/usr/local/mysql3306' > /dev/null
then
 # 一口气用了三个替换,分别为:
 # 第一步:将/usr/local/mysql3306转换为空
 # 第二步:将/share/mysql开头的/转换为空
 # 第三步:在share/mysql开头加上./,结果即:./share/mysql
 relpkgdata=`echo '/usr/local/mysql3306/share/mysql' | sed -e 's,^/usr/local/mysql3306,,' -e 's,^/,,' -e 's,^,./,'`
else
 relpkgdata='/usr/local/mysql3306/share/mysql'
fi

# 这一段都是在找mysqld文件,分别判断了libexec和bin目录
# 找不到就使用编译时的默认值
my_pwd=`pwd`
if test -n "$my_basedir_version" -a -d "$my_basedir_version"
then
 if test -x "$my_basedir_version/libexec/mysqld"
 then
  ledir="$my_basedir_version/libexec"
 else
  ledir="$my_basedir_version/bin"
 fi
# 这里对errmsg.sys文件进行了判断,个人认为这是为了确认当前目录为一个mysql安装基目录
elif test -f "$relpkgdata"/english/errmsg.sys -a -x "$my_pwd/bin/mysqld"
then
 my_basedir_version="$my_pwd"
 ledir="$my_pwd/bin"
elif test -f "$relpkgdata"/english/errmsg.sys -a -x "$my_pwd/libexec/mysqld"
then
 my_basedir_version="$my_pwd"
 ledir="$my_pwd/libexec"
else
 my_basedir_version='/usr/local/mysql3306'
 ledir='/usr/local/mysql3306/libexec'
fi

#
# 接下来是找到配置文件和数据文件目录
#

# 找到配置文件目录
# 我的是放在了etc/目录下,mysqld程序是会读取到的
#
# 可以从my_print_defaults脚本中获得默认的读取my.cnf顺序,如下
#   default options are read from the following files in the given order:
#   /etc/my.cnf /etc/mysql/my.cnf /home/mysql/mysql_master/etc/my.cnf ~/.my.cnf
# 或者可以使用strace -e open libexec/mysqld 2>&1 | grep my.cnf查看
if test -d $my_basedir_version/data/mysql
then
 datadir=$my_basedir_version/data
 if test -z "$defaults" -a -r "$datadir/my.cnf"
 then
  defaults="--defaults-extra-file=$datadir/my.cnf"
 fi
# 接下来找到数据文件的目录
elif test -d $my_basedir_version/var/mysql
then
 datadir=$my_basedir_version/var
# 找不到就用编译时指定的默认值
else
 datadir=/usr/local/mysql3306/var
fi

# 对存在两个配置文件情况进行冲突处理
if test -z "$mysql_home"
then 
 if test -r "$my_basedir_version/my.cnf" && test -r "$datadir/my.cnf"
 then
  # 优先考虑 $my_basedir_version/my.cnf 文件
  log_error "warning: found two instances of my.cnf -
$my_basedir_version/my.cnf and
$datadir/my.cnf
ignoring $datadir/my.cnf"

  mysql_home=$my_basedir_version
 elif test -r "$datadir/my.cnf"
 then
  log_error "warning: found $datadir/my.cnf
the data directory is a deprecated location for my.cnf, please move it to
$my_basedir_version/my.cnf"
  mysql_home=$datadir
 else
  mysql_home=$my_basedir_version
 fi
fi
export mysql_home

#
# 下面是使用bin/my_print_defaults读取my.cnf文件中的配置信息([mysqld] and [mysqld_safe])
# 并且和命令行中传入的参数进行合并

# 先是找到my_print_defaults执行文件 又是各种路径判断
if test -x "$my_basedir_version/bin/my_print_defaults"
then
 print_defaults="$my_basedir_version/bin/my_print_defaults"
elif test -x ./bin/my_print_defaults
then
 print_defaults="./bin/my_print_defaults"
elif test -x /usr/local/mysql3306/bin/my_print_defaults
then
 print_defaults="/usr/local/mysql3306/bin/my_print_defaults"
elif test -x /usr/local/mysql3306/bin/mysql_print_defaults
then
 print_defaults="/usr/local/mysql3306/bin/mysql_print_defaults"
else
 print_defaults="my_print_defaults"
fi

# 这个函数可以将一个指定的参数附加到$arg中(在此同时执行了转义操作)
append_arg_to_args () {
 args="$args "`shell_quote_string "$1"`
}

args=

# 这里set_user=2是针对下面一条parse_arguments来说的
# 因为如果在紧接着的parse_arugments函数中设置了--user的值,那么set_user就会变为1,表示--user以被配置
# 当然如果没有读取到--user的值,就是说--user没有配置,那么会在后面的if结构中设置set_user为0
# 这样在后面的判断结构中,set_user的值 0代表没有配置--user的值,1代表已经配置
set_user=2

# 解析配置文件中的参数,使用--loose-verbose来过滤[mysqld]和[server]组中的内容
parse_arguments `$print_defaults $defaults --loose-verbose mysqld server` 

if test $set_user -eq 2
then
 set_user=0
fi

# 又对[safe_mysqld]和[mysqld_safe]组中的内容进行了过滤读取
# 在我的配置文件中已经没有这两个组了,估计是为兼容旧版本的需要
parse_arguments `$print_defaults $defaults --loose-verbose mysqld_safe safe_mysqld`
# 用命令行输入选项 $@ 来覆盖配置文件中的选项 机智
parse_arguments pick-args-from-argv "$@"

#
# 下面是logging工具的使用
#
# 判断logger工具是否可用
if [ $want_syslog -eq 1 ]
then
 my_which logger > /dev/null 2>&1
 if [ $? -ne 0 ]
 then
  log_error "--syslog requested, but no 'logger' program found. please ensure that 'logger' is in your path, or do not specify the --syslog option to mysqld_safe."
  exit 1
 fi
fi

# 给err_log改名字。。。
if [ -n "$err_log" -o $want_syslog -eq 0 ]
then
 if [ -n "$err_log" ]
 then
 # 下面是为err_log添加一个.err后缀(如果现在名字没有后缀)
 # 如果不设置这个后缀,mysqld_safe和mysqld程序会将日志写入不同的文件中
 # 因为在 mysqld 程序中,它将识别带有.的文件名为错误日志(脚本注释上说的)

  # 这里的expr是识别文件名中“.”前面的字符总数量(包括.),如果没有设置后缀,返回就是0了
  if expr "$err_log" : '.*\.[^/]*$' > /dev/null
  then
    :
  else
   err_log="$err_log".err
  fi

  case "$err_log" in
   /* ) ;;
   * ) err_log="$datadir/$err_log" ;;
  esac
 else
  err_log=$datadir/`/bin/hostname`.err
 fi

 # 追加错误日志的位置选项
 append_arg_to_args "--log-error=$err_log"

 # 发出错误提示:不要使用syslog
 if [ $want_syslog -eq 1 ]
 then
  log_error "can't log to error log and syslog at the same time. remove all --log-error configuration options for --syslog to take effect."
 fi
 # log to err_log file
 log_notice "logging to '$err_log'."
 logging=files # 正式把logging改成files 使用错误日志来记录日志

# 这个分支就是使用syslog的方法了
else
 if [ -n "$syslog_tag" ]
 then
  # 设置各个syslog的使用标志位
  syslog_tag=`echo "$syslog_tag" | sed -e 's/[^a-za-z0-9_-]/_/g'`
  syslog_tag_mysqld_safe="${syslog_tag_mysqld_safe}-$syslog_tag"
  syslog_tag_mysqld="${syslog_tag_mysqld}-$syslog_tag"
 fi
 log_notice "logging to syslog."
 logging=syslog
fi

# 设置--user选项 
user_option=""
if test -w / -o "$user" = "root" # 根目录是否可写,或者当前用户为root
then
 if test "$user" != "root" -o $set_user = 1
 then
  user_option="--user=$user"
 fi
 # 创建错误日志,并将日志授权给指定的用户
 if [ $want_syslog -eq 0 ]; then
  touch "$err_log"
  chown $user "$err_log"
 fi
 # 这里它还对当前用户做了ulimit设置,包括可以打开的文件数量--open_files-limit选项
 if test -n "$open_files"
 then
  ulimit -n $open_files
  append_arg_to_args "--open-files-limit=$open_files"
 fi
fi

safe_mysql_unix_port={mysql_unix_port:-${mysql_unix_port:-/usr/local/mysql3306/tmp/mysql.sock}}
# 确保 $safe_mysql_unix_port 目录是存在的
mysql_unix_port_dir=`dirname $safe_mysql_unix_port`
if [ ! -d $mysql_unix_port_dir ]
then
 mkdir $mysql_unix_port_dir
 chown $user $mysql_unix_port_dir
 chmod 755 $mysql_unix_port_dir
fi

# 如果用户没有制定mysqld程序的名称,这里就默认赋值为mysqld
if test -z "$mysqld"
then
 mysqld=mysqld
fi

# 下面几段分别是对 mysqld , pid , port文件选项的检查和设置,省略100个字
if test ! -x "$ledir/$mysqld"
then
 log_error "the file $ledir/$mysqld
does not exist or is not executable. please cd to the mysql installation
directory and restart this script from there as follows:
./bin/mysqld_safe&
see http://dev.mysql.com/doc/mysql/en/mysqld-safe.html for more information"
 exit 1
fi

if test -z "$pid_file"
then
 pid_file="$datadir/`/bin/hostname`.pid"
else
 case "$pid_file" in
  /* ) ;;
  * ) pid_file="$datadir/$pid_file" ;;
 esac
fi
append_arg_to_args "--pid-file=$pid_file"

if test -n "$mysql_unix_port"
then
 append_arg_to_args "--socket=$mysql_unix_port"
fi
if test -n "$mysql_tcp_port"
then
 append_arg_to_args "--port=$mysql_tcp_port"
fi

#
# 接下来是关于优先级的设置
#

if test $niceness -eq 0
then
 nohup_niceness="nohup"
else
 nohup_niceness="nohup nice -$niceness"
fi

# 将当前的默认优先级设置为0
if nohup nice > /dev/null 2>&1
then
  # normal_niceness记载默认的调度优先级
  normal_niceness=`nice`
  # nohup_niceness记载使用nohup执行方式的调度优先级
  nohup_niceness=`nohup nice 2>/dev/null`

  numeric_nice_values=1
  # 这个for是为了检查$normal_niceness $nohup_niceness两个变量值的合法性
  for val in $normal_niceness $nohup_niceness
  do
    case "$val" in
      -[0-9] | -[0-9][0-9] | -[0-9][0-9][0-9] | \
       [0-9] | [0-9][0-9] | [0-9][0-9][0-9] )
        ;;
      * )
        numeric_nice_values=0 ;;
    esac
  done

  # 这个判断结构很重要
  # 它保证了使用nohup执行的mysqld程序在调度优先级上不会低于直接执行mysqld程序的方式
  if test $numeric_nice_values -eq 1
  then
    nice_value_diff=`expr $nohup_niceness - $normal_niceness`
    if test $? -eq 0 && test $nice_value_diff -gt 0 && \
      nice --$nice_value_diff echo testing > /dev/null 2>&1
    then
      # 进入分支说明$nohup_niceness的值比$normal_niceness大,即nohup执行方式调度优先级比正常执行方式低
      # 这是不希望看到的,所以下面就人为的提升了nohup的优先级(降低niceness的值)
      niceness=`expr $niceness - $nice_value_diff`
      nohup_niceness="nice -$niceness nohup"
    fi
  fi
else
  # 下面是测试nohup在当前系统中是否可用,不可用的话就置空nohup_niceness
  if nohup echo testing > /dev/null 2>&1
  then
    :
  else
    nohup_niceness=""
  fi
fi

# 指定内核文件大小
if test -n "$core_file_size"
then
 ulimit -c $core_file_size
fi

#
# 如果已经存在一个pid文件,则检查是否有已经启动的mysqld_safe进程
if test -f "$pid_file"
then
 pid=`cat "$pid_file"`
 if /bin/kill -0 $pid > /dev/null 2> /dev/null
 then
  if /bin/ps wwwp $pid | grep -v " grep" | grep -v mysqld_safe | grep -- "$mysqld" > /dev/null
  then
   log_error "a mysqld process already exists"
   exit 1
  fi
 fi
 # 下面是处理办法:删除旧的pid文件并报错
 rm -f "$pid_file"
 if test -f "$pid_file"
 then
  log_error "fatal error: can't remove the pid file:
$pid_file
please remove it manually and start $0 again;
mysqld daemon not started"
  exit 1
 fi
fi

#
# 下面便是拼接执行语句运行了。
#

cmd="$nohup_niceness"

# 检查一下命令 并进行转义操作
for i in "$ledir/$mysqld" "$defaults" "--basedir=$my_basedir_version" \
 "--datadir=$datadir" "$user_option"
do
 cmd="$cmd "`shell_quote_string "$i"`
done
cmd="$cmd $args"
# avoid 'nohup: ignoring input' warning
test -n "$nohup_niceness" && cmd="$cmd < /dev/null"

log_notice "starting $mysqld daemon with databases from $datadir"

# 后台循环 执行mysqld
while true
do
 rm -f $safe_mysql_unix_port "$pid_file"	# 保险起见,又删除了一次pid文件

 # 调用eval_log_error函数,传入$cmd参数的值,最后使用eval命令执行了启动mysqld
 eval_log_error "$cmd"

 if test ! -f "$pid_file"		# 没有成功创建pid文件,则退出分支
 then
  break
 fi

 # mysqld_safe已经启动的处理方法,保证只有一个mysqld_safe程序启动
 if true && test $kill_mysqld -eq 1
 then
  # 统计启动的mysqld进程的数目
  numofproces=`ps xaww | grep -v "grep" | grep "$ledir/$mysqld\>" | grep -c "pid-file=$pid_file"`

  log_notice "number of processes running now: $numofproces"
  i=1
  while test "$i" -le "$numofproces"
  do 
   # 这个proc的数据即是ps mysqld_safe程序的输出 第一个数字即为进程id
   proc=`ps xaww | grep "$ledir/$mysqld\>" | grep -v "grep" | grep "pid-file=$pid_file" | sed -n '$p'` 
   # 使用t来获取进程id
   for t in $proc
   do
    break
   done
   # kill掉该个mysqld_safe程序
   if kill -9 $t
   then
    log_error "$mysqld process hanging, pid $t - killed"
   else
    break
   fi
   # 每干掉一个mysqld_safe就把i加一,这样没有多余的mysqld_safe时就可以跳出循环了
   i=`expr $i + 1`
  done
 fi
 log_notice "mysqld restarted"
done

# 完结撒花
log_notice "mysqld from pid file $pid_file ended"