作者归档:songtianlun

使用 GDB 调试 Go 程序

进入调试

使用前,请先确保机器上已经安装 GDB

[root@localhost code]# which gdb /usr/bin/gdb

准备就绪后,以下列测试程序为例

package main

import "fmt"

func main(){
  msg := "hello, world"
  fmt.Println(msg)
}

然后执行 如下命令进行编译,里面有好多个参数,有疑问的可以自行搜索引擎

# 关闭内联优化,方便调试
$ go build -gcflags "-N -l" demo.go

# 发布版本删除调试符号
go build -ldflags “-s -w”

最后使用 GDB 命令进入调试界面

# tui 界面
$ gdb -tui demo 
# 纯终端界面 
$ gdb demo

进入 后就可以打断点调试了

(gdb) b main.main   # 在 main 包里的 main 函数 加断点
Breakpoint 1 at 0x4915c0: file /home/wangbm/code/demo.go, line 5.
(gdb) run  # 执行进程
Starting program: /home/wangbm/code/demo 
Breakpoint 1, main.main () at /home/wangbm/code/demo.go:5
(gdb) 

# 注,如果需要带参数运行,比如正常这样运行:
./app -a 1 --debug
# 在 gdb 中就这样运行
gdb app
(gdb) run -a 1 --debug

调试指令

要熟练使用 GDB ,得熟悉的掌握它的指令,这里列举一下

  • r:run,执行程序
  • n:next,下一步,不进入函数
  • s:step,下一步,会进入函数
  • b:breakponit,设置断点
  • l:list,查看源码
  • c:continue,继续执行到下一断点
  • bt:backtrace,查看当前调用栈
  • p:print,打印查看变量
  • q:quit,退出 GDB
  • whatis:查看对象类型
  • info breakpoints:查看所有的断点
  • info locals:查看局部变量
  • info args:查看函数的参数值及要返回的变量值
  • info frame:堆栈帧信息
  • info goroutines:查看 goroutines 信息。在使用前 ,需要注意先执行 source /usr/local/go/src/runtime/runtime-gdb.py
  • goroutine 1 bt:查看指定序号的 goroutine 调用堆栈
  • 回车:重复执行上一次操作

其中有几个指令的使用比较灵活

# 查看指定行数上下5行
(gdb) l 8

# 查看指定范围的行数
(gdb) l 5:8

# 查看指定文件的行数上下5行
l demo.go:8

# 可以查看函数,记得加包名
l main.main

把上面的 l 换成 b ,大多数也同样适用

# 在指定行打断点 
(gdb) b 8  
# 在指定指定文件的行打断点 
b demo.go:8  
# 在指定函数打断点,记得加包名 
b main.main

还有 p – print,打印变量

# 查看变量
(gdb) p var

# 查看对象长度或容量
(gdb) p $len(var)
(gdb) p $cap(var)

# 查看对象的动态类型
(gdb) p $dtype(var)
(gdb) iface var

# 举例如下
(gdb) p i
$4 = {str = "cbb"}
(gdb) whatis i
type = regexp.input
(gdb) p $dtype(i)
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) iface i
regexp.input: struct regexp.inputBytes *

以上就是关于 GDB 的使用方法,非常简单,可以自己手动敲下体验一下。

References

Linux overcommit 及 oom-killer 机制

通常是因为某时刻应用程序大量请求内存导致系统内存不足造成的,这通常会触发 Linux 内核里的 Out of Memory (OOM) killer,OOM killer 会杀掉某个进程(用户态进程,不是内核线程)以腾出内存留给系统用,不致于让系统立刻崩溃。

overcommit

Linux 内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没有实际全部使用,为了提高性能,这部分没用的内存可以留作它用,这部分内存是属于每个进程的,内核直接回收利用的话比较麻烦,所以内核采用一种过度分配内存(over-commit memory)的办法来间接利用这部分 “空闲” 的内存,提高整体内存的使用效率。一般来说这样做没有问题,但当大多数应用程序都消耗完自己的内存的时候麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。

/proc/sys/vm/overcommit_memory 取值为[0-2],默认值为0:  

0: 启发式过度使用处理,显而易见的过度使用地址空间被拒绝。它在允许的情况下确保 严重分配失败、过度使用以减少交换使用。  
1: 始终过度使用内存,表示kernel永远不会检查是否有足够的内存可用,总是返回true.  
2: 禁止过度使用,表示kernel拒绝 >= 可用的swap+物理内存 * overcommit_ratio(默认为50)的内存分配请求.  
在大多数情况下,这意味着访问页面时不会终止进程,但会在适当时收到内存分配错误。

#16 GB Swap, 16 GB RAM, overcommit_memory=2 内存请求上限及计算方法
# free
              total        used        free      shared  buff/cache   available
Mem:       16311328     6048244      573316       42992     9689768     8963032
Swap:      16601084     3580760    13020324

# 计算方法
cat /proc/sys/vm/overcommit_ratio #默认50
Mem * overcommit_ratio (50%) + swap= 8155664 + 16601084 = 24756748 kB

16G+16G*50%/100=24G (overcommit_memory = 2)
若修改vm.overcommit_raito为100,则请求内存16G+16G*100%/100=32G

# grep -i commit /proc/meminfo
CommitLimit:    24756748 kB
Committed_AS:   14178044 kB

cat << EOF >> /etc/sysctl.conf
vm.overcommit_memory=1 #redis
vm.overcommit_ratio=50 # 默认
EOF
sysctl -p

oom killer

查看oom killer 日志,最常见的就是MySQL 无缘无故挂掉,Out of memory: Kill process信息:

grep -i "kill" /var/log/messages #CentOS
#grep -i "kill" /var/log/kern.log #Ubuntu
...
Out of memory: Kill process 9682 (mysqld) score 9 or sacrifice child
Killed process 9682, UID 27, (mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kB
httpd invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
httpd cpuset=/ mems_allowed=0
Pid: 8911, comm: httpd Not tainted 2.6.32-279.1.1.el6.i686 #1
...

内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码 linux/mm/oom_kill.c,该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。每个进程的点数跟oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

out_of_memory() 被触发,然后调用 select_bad_process() 选择一个 “bad” 进程杀掉,挑选的过程由 oom_badness() 决定,挑选的算法和想法都很简单很朴实:最 bad 的那个进程就是那个最占用内存的进程。

/**
 * oom_badness - heuristic function to determine which candidate task to kill
 * @p: task struct of which task we should calculate
 * @totalpages: total present RAM allowed for page allocation
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{
    long points;
    long adj;

    if (oom_unkillable_task(p, memcg, nodemask))
        return 0;

    p = find_lock_task_mm(p);
    if (!p)
        return 0;

    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN) {
        task_unlock(p);
        return 0;
    }

    /*
     * The baseline for the badness score is the proportion of RAM that each
     * task's rss, pagetable and swap space use.
     */
    points = get_mm_rss(p->mm) + p->mm->nr_ptes +
         get_mm_counter(p->mm, MM_SWAPENTS);
    task_unlock(p);

    /*
     * Root processes get 3% bonus, just like the __vm_enough_memory()
     * implementation used by LSMs.
     */
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))
        adj -= 30;

    /* Normalize to oom_score_adj units */
    adj *= totalpages / 1000;
    points += adj;

    /*
     * Never return 0 for an eligible task regardless of the root bonus and
     * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
     */
    return points > 0 ? points : 1;
}

上面代码里的注释写的很明白,理解了这个算法我们就理解了为啥 MySQL 躺着也能中枪了,因为它的体积总是最大(一般来说它在系统上占用内存最多),所以如果 Out of Memeory (OOM) 的话总是不幸第一个被 kill 掉。解决这个问题最简单的办法就是增加内存,或者想办法优化 MySQL 使其占用更少的内存,除了优化 MySQL 外还可以优化系统,让系统尽可能使用少的内存以便应用程序(如 MySQL) 能使用更多的内存,还有一个临时的办法就是调整内核参数,让 MySQL 进程不容易被 OOM killer 发现。

找出最有可能被 OOM Killer 杀掉的进程

# cat /data/shell/oomscore.sh 
#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n" \
        "$(cat $proc/oom_score)" \
        "$(basename $proc)" \
        "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

调整oom_adj

`/proc/

/oom_adj` ​值范围是[-17, 15],oom_score 值越高越容易被oom kill掉。设为 `-17`则该进程禁用 oom_killer。 比如查看进程号为187418的 omm_score,这个分数被上面提到的 omm_score_adj 参数调整后(-15),就变成了3: “`bash pidof mysqld 187418 # cat /proc/187418/oom_score 18 # echo -15 > /proc/187418/oom_score_adj # cat /proc/981/oom_score 3 “` # 配置oom killer 我们可以通过一些内核参数来调整 OOM killer 的行为,避免系统在那里不停的杀进程。比如我们可以在触发 OOM 后立刻触发 kernel panic,kernel panic 10秒后自动重启系统。 > 修改panic_on_oom值为1,表示请求内存不足时10秒后重启系统 “`bash cat <> /etc/sysctl.conf vm.panic_on_oom=1 kernel.panic=10 # 表示10s后重启 EOF sysctl -p “` # 内核参数 “`bash /proc/sys/vm/panic_on_oom 取值为[0-2],默认值为0: 0: OOM时系统执行OOM Killer 1: OOM时系统会panic(恐慌) 2: OOM时系统一定会触发panic(恐慌) /proc/sys/vm/oom_kill_allocating_task 取值为[0-1],默认值为0: 0: 内核将检查每个进程的分数,分数最高的进程将被kill掉 1: 那么内核将kill掉当前申请内存的进程 “` # References – [Linux overcommit 及 oom-killer 机制](https://sundayle.sundayhk.com/linux-overcommit-oom-killer/) – [理解和配置 Linux 下的 OOM Killer](https://www.vpsee.com/2013/10/how-to-configure-the-linux-oom-killer/) – [kernel overcommit accounting](https://www.kernel.org/doc/Documentation/vm/overcommit-accounting) – [Virtual memory settings in Linux – The Problem with Overcommit](http://engineering.pivotal.io/post/virtual_memory_settings_in_linux_-_the_problem_with_overcommit/)

基于 listmonk 实现 rss to mail

listmonk 部署

安装 官方教程 进行即可,大致如下:

# Download the compose file to the current directory.
curl -LO https://github.com/knadh/listmonk/raw/master/docker-compose.yml

# Run the services in the background.
docker compose up -d

rss to mail 脚本

主程序 main.py

    import feedparser
    import requests
    import json
    import os
    import logging
    from time import sleep
    from dateutil import parser
    from typing import List, Dict
    import re

    # 配置日志
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('rss_checker.log'),
            logging.StreamHandler()
        ]
    )
    logger = logging.getLogger(__name__)

    RSS_URL = os.getenv('RSS_URL', "https://xxx.com/feed/")
    LISTMONK_API_URL = os.getenv('LISTMONK_API_URL', "https://listmonk.xxx.com/api/campaigns")
    LISTMONK_TOKEN = os.getenv('LISTMONK_TOKEN', "bot:xxx")
    LISTMONK_SEND_LIST_ID = int(os.getenv('LISTMONK_SEND_LIST_ID', 4))
    LISTMONK_SEND_LIST_IDS = [LISTMONK_SEND_LIST_ID]

    class RSSChecker:
        def __init__(self):
            self.rss_url = RSS_URL
            self.listmonk_url = LISTMONK_API_URL
            self.headers = {
                "Content-Type": "application/json",
                "Authorization": "token" + LISTMONK_TOKEN
            }
            self.max_retries = 3
            self.retry_delay = 5  # seconds

        def clean_html_content(self, html_content: str) -> str:
            """清理HTML内容,移除以http://或https://开头的内容"""
            try:
                if not html_content:
                    return ""

                # 移除以http://或https://开头的内容
                cleaned_content = re.sub(r'https?://\S+', '', html_content)

                # 清理多余的空白字符
                cleaned_content = re.sub(r'\s+', ' ', cleaned_content).strip()

                return cleaned_content

            except Exception as e:
                logger.error(f"清理HTML内容时出错: {str(e)}")
                return html_content  # 如果处理失败,返回原始内容

        def get_last_check_time(self) -> str:
            try:
                with open('last_check.txt', 'r') as f:
                    last_time = f.read().strip()
                    logger.debug(f"读取到上次检查时间: {last_time}")
                    return last_time
            except:
                logger.warning("未找到上次检查时间文件")
                return ''

        def save_check_time(self, time: str) -> None:
            try:
                with open('last_check.txt', 'w') as f:
                    f.write(time)
                logger.debug(f"保存本次检查时间: {time}")
            except Exception as e:
                logger.error(f"保存检查时间时出错: {str(e)}")

        def create_email_content(self, entries: List[Dict]) -> str:
            """创建美化的HTML邮件内容"""
            html_content = """

<style>
                .header {
                    text-align: center;
                    margin-bottom: 40px;
                    padding: 20px;
                    background-color: #f8f9fa;
                    border-radius: 8px;
                }
                .main-title {
                    font-size: 28px;
                    color: #2c3e50;
                    margin-bottom: 10px;
                }
                .subtitle {
                    font-size: 20px;
                    color: #34495e;
                    margin-bottom: 15px;
                }
                .blog-name {
                    font-size: 24px;
                    color: #16a085;
                    margin-bottom: 10px;
                }
                .blog-description {
                    font-size: 16px;
                    color: #7f8c8d;
                    margin-bottom: 20px;
                }
                .article-container {
                    font-family: Arial, sans-serif;
                    max-width: 800px;
                    margin: 0 auto;
                    padding: 20px;
                }
                .article {
                    margin-bottom: 30px;
                    border-bottom: 1px solid #eee;
                    padding-bottom: 20px;
                }
                .article-title {
                    color: #333;
                    font-size: 24px;
                    margin-bottom: 10px;
                }
                .article-summary {
                    color: #666;
                    line-height: 1.6;
                    margin-bottom: 15px;
                }
                .read-more {
                    display: inline-block;
                    padding: 8px 15px;
                    background-color: #4CAF50;
                    color: white;
                    text-decoration: none;
                    border-radius: 4px;
                }
                .read-more:hover {
                    background-color: #45a049;
                }
            </style>
            <div class="article-container">
                <div class="header">
                <h1 class="main-title">烹茶室(Oskyla 晴空阁) 更新了!</h1>
                    <h2 class="subtitle">欢迎访问 Frytea's Blog</h2>
                    <h3 class="blog-name">Oskyla 烹茶室</h3>
                    <p class="blog-description">价值信息藏书阁,统一门户入口。</p>
                </div>
            """

            for entry in entries:
                # 清理文章标题和摘要中的HTML内容
                clean_title = self.clean_html_content(entry.title)
                clean_summary = self.clean_html_content(entry.summary)

                html_content += f"""
                <div class="article">
                    <h2 class="article-title">{clean_title}</h2>
                    <div class="article-summary">{clean_summary}</div>
                    <a href="{entry.link}" class="read-more">阅读全文</a>
                </div>
                """

            html_content += "</div>"
            return html_content

        def publish_campaign(self, campaign_id: int) -> bool:
            for attempt in range(self.max_retries):
                try:
                    publish_url = f"{self.listmonk_url}/{campaign_id}/status"
                    response = requests.put(
                        publish_url,
                        headers=self.headers,
                        json={"status": "running"}
                    )

                    if response.status_code == 200:
                        logger.info(f"活动 {campaign_id} 发布成功")
                        return True

                    logger.warning(f"发布尝试 {attempt + 1} 失败: HTTP {response.status_code}")
                    if attempt < self.max_retries - 1:
                        sleep(self.retry_delay)

                except requests.exceptions.RequestException as e:
                    logger.error(f"发布API请求异常: {str(e)}")
                    if attempt < self.max_retries - 1:
                        sleep(self.retry_delay)

            return False

        def send_newsletter(self, new_entries: List[Dict]) -> bool:
            try:
                content = self.create_email_content(new_entries)
                # 获取文章数量
                article_count = len(new_entries)
                # 清理标题中的HTML内容
                #titles = ", ".join(self.clean_html_content(entry.title) for entry in new_entries)

                data = {
                    "name": "Frytea's Blog 更新通知",
                    "subject": f"Frytea's Blog 更新了 {article_count} 篇新文章",
                    "lists": LISTMONK_SEND_LIST_IDS,
                    "content_type": "html",
                    "body": content,
                    "type": "regular"
                }

                logger.debug("准备发送的数据: %s", json.dumps(data, indent=2))

                response = requests.post(self.listmonk_url, headers=self.headers, json=data)
                if response.status_code == 200:
                    campaign_id = response.json().get('data', {}).get('id')
                    if campaign_id:
                        return self.publish_campaign(campaign_id)

                logger.error(f"创建活动失败: HTTP {response.status_code}")
                return False

            except Exception as e:
                logger.error(f"发送邮件时出错: {str(e)}")
                return False

        def check_and_send(self) -> None:
            try:
                logger.info(f"开始解析RSS源: {self.rss_url}")
                feed = feedparser.parse(self.rss_url)

                if feed.bozo:
                    logger.error(f"RSS解析错误: {feed.bozo_exception}")
                    return

                if not feed.entries:
                    logger.warning("RSS源没有任何条目")
                    return

                last_check = self.get_last_check_time()
                new_entries = []

                #for entry in feed.entries:
                #    if not last_check or entry.published > last_check:
                #        new_entries.append(entry)

                for entry in feed.entries:
                    # 将字符串解析为 datetime 对象
                    entry_time = parser.parse(entry.published)
                    last_check_time = parser.parse(last_check) if last_check else None

                    if not last_check_time or entry_time > last_check_time:
                        new_entries.append(entry)

                if new_entries:
                    logger.info(f"检测到 {len(new_entries)} 篇新文章")
                    if self.send_newsletter(new_entries):
                        self.save_check_time(max(entry.published for entry in new_entries))
                else:
                    logger.info("没有新文章")

            except Exception as e:
                logger.error(f"执行过程中出现未预期的错误: {str(e)}", exc_info=True)

    if __name__ == "__main__":
        checker = RSSChecker()
        checker.check_and_send()

依赖 requirements.txt

beautifulsoup4==4.12.3
feedparser==6.0.11
Requests==2.32.3

为方便使用的 Makefile

all: broadcast  

broadcast: venv  
    venv/bin/python3 main.py  

venv:  
    python3 -m venv venv  
    venv/bin/pip3 install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple

定时触发脚本,定时运行即可,修改为自己的内容。

export RSS_URL=https://xxx.com/feed/
export LISTMONK_API_URL=https://listmonk.xxx.com/api/campaigns
export LISTMONK_TOKEN=apiusername:api-token
export LISTMONK_SEND_LIST_ID=3

cd /mnt/data/script/listmonk_RSS_to_mail &&  make 

效果展示

邮件效果展示

欢迎订阅:https://frytea.com/subscribe.html

References

listmonk 导入 Mailchimp 邮件清单

使用单行 perl 脚本将 Mailchimp 导出的数据转换为 listmonk 可用的清单。

perl -e 'print qq{email,name,attributes\n};while(<>){ my @r = split /,/; next unless $r[0] =~ /@/; map { s/"//g } @r; my $name = $r[1]; $name .= " $r[2]" if $r[2]; $name ||= "Unknown Name"; print qq{$r[0],"$name","{""mailchimp"": true}"\n}}' subscribed_segment_export_xxxxx.csv 

References

Docker 部署 mautic 并增加插件和翻译包等

Docker 部署方法

参考:https://github.com/mautic/docker-mautic/tree/mautic5/examples

增加插件

使用如下 Dockerfile

FROM mautic/mautic:5-apache

COPY ./plugins/ /var/www/html/docroot/plugins/

结合以下 Makefile

all:
        docker build -t mautic/mautic:5-apache-my .

整个目录架构是这样:

root@tencent-gz1:/data/docker/mautic/add-something# tree -L 2 .
.
├── Dockerfile
├── Makefile
├── plugins
│   └── MauticRssToEmailBundle
└── translations
    └── zh_CN.zip

执行

增加语言包

Dockerfile 增加一个目录:

FROM mautic/mautic:5-apache

COPY ./plugins/ /var/www/html/docroot/plugins/
COPY ./translations/ /var/www/html/docroot/translations/

之后将语言包放入 translations 再构建新镜像即可。

比如下载简体中文类似这样做 :

wget https://language-packs.mautic.com/zh_CN.zip
unzip zh_CN.zip
mv zh_CN ./translations/

常见问题

500

root@tencent-gz1:/data/docker/mautic# docker exec -it basic-mautic_web-1 bash
root@f1088e8096c1:/var/www/html/docroot# cd ..
root@f1088e8096c1:/var/www/html# php bin/console cache:clear
root@7756b780cd1c:/var/www/html# php bin/console cache:clear
// Clearing the cache for the prod environment with debug false
[OK] Cache for the "prod" environment (debug=false) was successfully cleared. 

References

Ceph RBD 查看实际占用top

rbd du -p ssd | awk '  
NR>1 {  
   size=$4  
   unit=$5  
   if (unit=="MiB") size=size  
   else if (unit=="GiB") size=size*1024  
   else if (unit=="TiB") size=size*1024*1024  
   print size " " unit " " $0  
}' | sort -nr | head -n 50 | cut -d" " -f3-

by claude 3.5

效果:

root@pve1:~# rbd du -p ssd | awk '  
NR>1 {  
   size=$4  
   unit=$5  
   if (unit=="MiB") size=size  
   else if (unit=="GiB") size=size*1024  
   else if (unit=="TiB") size=size*1024*1024  
   print size " " unit " " $0  
}' | sort -nr | head -n 50 | cut -d" " -f3-  

<TOTAL>                                              111 TiB    47 TiB  
vm-1033-disk-0                                        20 TiB    20 TiB  
vm-1054-disk-1@bak20240731                          1000 GiB   677 GiB  
vm-503-disk-3                                        500 GiB   500 GiB  
vm-502-disk-3                                        500 GiB   500 GiB  
vm-501-disk-3                                        500 GiB   500 GiB  
vm-1054-disk-1@bak20231106                          1000 GiB   475 GiB  
vm-497-disk-0@backup0530                             500 GiB   340 GiB  
vm-1279-disk-1                                       320 GiB   320 GiB  
vm-1090-disk-0                                       300 GiB   300 GiB

Bash 条件判断

本章介绍 Bash 脚本的条件判断语法。

if 结构

if是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下。

if commands; then
  commands
[elif commands; then
  commands...]
[else
  commands]
fi

这个命令分成三个部分:ifelifelse。其中,后两个部分是可选的。

if关键字后面是主要的判断条件,elif用来添加在主条件不成立时的其他判断条件,else则是所有条件都不成立时要执行的部分。

if test $USER = "foo"; then
  echo "Hello foo."
else
  echo "You are not foo."
fi

上面的例子中,判断条件是环境变量$USER是否等于foo,如果等于就输出Hello foo.,否则输出其他内容。

ifthen写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。

if true
then
  echo 'hello world'
fi

if false
then
  echo 'it is false' # 本行不会执行
fi

上面的例子中,truefalse是两个特殊命令,前者代表操作成功,后者代表操作失败。if true意味着命令部分总是会执行,if false意味着命令部分永远不会执行。

除了多行的写法,if结构也可以写成单行。

$ if true; then echo 'hello world'; fi
hello world

$ if false; then echo "It's true."; fi

注意,if关键字后面也可以是一条命令,该条命令执行成功(返回值),就意味着判断条件成立。

$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world

上面命令中,if后面是一条命令echo 'hi'。该命令会执行,如果返回值是,则执行then的部分。

if后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回,就会执行then的部分。

$ if false; true; then echo 'hello world'; fi
hello world

上面例子中,if后面有两条命令(false;true;),第二条命令(true)决定了then的部分是否会执行。

elif部分可以有多个。

#!/bin/bash

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
if [ "$character" = "1" ]; then
    echo 1
elif [ "$character" = "2" ]; then
    echo 2
elif [ "$character" = "3" ]; then
    echo 3
else
    echo 输入不符合要求
fi

上面例子中,如果用户输入3,就会连续判断3次。

test 命令

if结构的判断条件,一般使用test命令,有三种形式。

# 写法一
test expression

# 写法二
[ expression ]

# 写法三
[[ expression ]]

上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。

上面的expression是一个表达式。这个表达式为真,test命令执行成功(返回值为);表达式为伪,test命令执行失败(返回值为1)。注意,第二种和第三种写法,[]与内部的表达式之间必须有空格。

$ test -f /etc/hosts
$ echo $?
0

$ [ -f /etc/hosts ]
$  echo $?
0

上面的例子中,test命令采用两种写法,判断/etc/hosts文件是否存在,这两种写法是等价的。命令执行后,返回值为,表示该文件确实存在。

实际上,[这个字符是test命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。

下面把test命令的三种形式,用在if结构中,判断一个文件是否存在。

# 写法一
if test -e /tmp/foo.txt ; then
  echo "Found foo.txt"
fi

# 写法二
if [ -e /tmp/foo.txt ] ; then
  echo "Found foo.txt"
fi

# 写法三
if [[ -e /tmp/foo.txt ]] ; then
  echo "Found foo.txt"
fi

判断表达式

if关键字后面,跟的是一个命令。这个命令可以是test命令,也可以是其他命令。命令的返回值为表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。

常用的判断表达式有下面这些。

文件判断

以下表达式用来判断文件状态。

  • [ -a file ]:如果 file 存在,则为true
  • [ -b file ]:如果 file 存在并且是一个块(设备)文件,则为true
  • [ -c file ]:如果 file 存在并且是一个字符(设备)文件,则为true
  • [ -d file ]:如果 file 存在并且是一个目录,则为true
  • [ -e file ]:如果 file 存在,则为true
  • [ -f file ]:如果 file 存在并且是一个普通文件,则为true
  • [ -g file ]:如果 file 存在并且设置了组 ID,则为true
  • [ -G file ]:如果 file 存在并且属于有效的组 ID,则为true
  • [ -h file ]:如果 file 存在并且是符号链接,则为true
  • [ -k file ]:如果 file 存在并且设置了它的“sticky bit”,则为true
  • [ -L file ]:如果 file 存在并且是一个符号链接,则为true
  • [ -N file ]:如果 file 存在并且自上次读取后已被修改,则为true
  • [ -O file ]:如果 file 存在并且属于有效的用户 ID,则为true
  • [ -p file ]:如果 file 存在并且是一个命名管道,则为true
  • [ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为true
  • [ -s file ]:如果 file 存在且其长度大于零,则为true
  • [ -S file ]:如果 file 存在且是一个网络 socket,则为true
  • [ -t fd ]:如果 fd 是一个文件描述符,并且重定向到终端,则为true。 这可以用来判断是否重定向了标准输入/输出/错误。
  • [ -u file ]:如果 file 存在并且设置了 setuid 位,则为true
  • [ -w file ]:如果 file 存在并且可写(当前用户拥有可写权限),则为true
  • [ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
  • [ FILE1 -nt FILE2 ]:如果 FILE1 比 FILE2 的更新时间更近,或者 FILE1 存在而 FILE2 不存在,则为true
  • [ FILE1 -ot FILE2 ]:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true
  • [ FILE1 -ef FILE2 ]:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为true

下面是一个示例。

#!/bin/bash

FILE=~/.bashrc

if [ -e "$FILE" ]; then
  if [ -f "$FILE" ]; then
    echo "$FILE is a regular file."
  fi
  if [ -d "$FILE" ]; then
    echo "$FILE is a directory."
  fi
  if [ -r "$FILE" ]; then
    echo "$FILE is readable."
  fi
  if [ -w "$FILE" ]; then
    echo "$FILE is writable."
  fi
  if [ -x "$FILE" ]; then
    echo "$FILE is executable/searchable."
  fi
else
  echo "$FILE does not exist"
  exit 1
fi

上面代码中,$FILE要放在双引号之中,这样可以防止变量$FILE为空,从而出错。因为$FILE如果为空,这时[ -e $FILE ]就变成[ -e ],这会被判断为真。而$FILE放在双引号之中,[ -e "$FILE" ]就变成[ -e "" ],这会被判断为伪。

字符串判断

以下表达式用来判断字符串。

  • [ string ]:如果string不为空(长度大于0),则判断为真。
  • [ -n string ]:如果字符串string的长度大于零,则判断为真。
  • [ -z string ]:如果字符串string的长度为零,则判断为真。
  • [ string1 = string2 ]:如果string1string2相同,则判断为真。
  • [ string1 == string2 ] 等同于[ string1 = string2 ]
  • [ string1 != string2 ]:如果string1string2不相同,则判断为真。
  • [ string1 '>' string2 ]:如果按照字典顺序string1排列在string2之后,则判断为真。
  • [ string1 '<' string2 ]:如果按照字典顺序string1排列在string2之前,则判断为真。

注意,test命令内部的><,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。

下面是一个示例。

#!/bin/bash

ANSWER=maybe

if [ -z "$ANSWER" ]; then
  echo "There is no answer." >&2
  exit 1
fi
if [ "$ANSWER" = "yes" ]; then
  echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
  echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
  echo "The answer is MAYBE."
else
  echo "The answer is UNKNOWN."
fi

上面代码中,首先确定$ANSWER字符串是否为空。如果为空,就终止脚本,并把退出状态设为1。注意,这里的echo命令把错误信息There is no answer.重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER字符串不为空,就判断它的值是否等于yesno或者maybe

注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ],否则变量替换成字符串以后,test命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ],这时会判断为真。如果放在双引号之中,[ -n "" ]就判断为伪。

整数判断

下面的表达式用于判断整数。

  • [ integer1 -eq integer2 ]:如果integer1等于integer2,则为true
  • [ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true
  • [ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true
  • [ integer1 -lt integer2 ]:如果integer1小于integer2,则为true
  • [ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true
  • [ integer1 -gt integer2 ]:如果integer1大于integer2,则为true

下面是一个用法的例子。

#!/bin/bash

INT=-5

if [ -z "$INT" ]; then
  echo "INT is empty." >&2
  exit 1
fi
if [ $INT -eq 0 ]; then
  echo "INT is zero."
else
  if [ $INT -lt 0 ]; then
    echo "INT is negative."
  else
    echo "INT is positive."
  fi
  if [ $((INT % 2)) -eq 0 ]; then
    echo "INT is even."
  else
    echo "INT is odd."
  fi
fi

上面例子中,先判断变量$INT是否为空,然后判断是否为,接着判断正负,最后通过求余数判断奇偶。

正则判断

[[ expression ]]这种判断形式,支持正则表达式。

[[ string1 =~ regex ]]

上面的语法中,regex是一个正则表示式,=~是正则比较运算符。

下面是一个例子。

#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  echo "INT is an integer."
  exit 0
else
  echo "INT is not an integer." >&2
  exit 1
fi

上面代码中,先判断变量INT的字符串形式,是否满足^-?[0-9]+$的正则模式,如果满足就表明它是一个整数。

test 判断的逻辑运算

通过逻辑运算,可以把多个test判断表达式结合起来,创造更复杂的判断。三种逻辑运算ANDOR,和NOT,都有自己的专用符号。

  • AND运算:符号&&,也可使用参数-a
  • OR运算:符号||,也可使用参数-o
  • NOT运算:符号!

下面是一个AND的例子,判断整数是否在某个范围之内。

#!/bin/bash

MIN_VAL=1
MAX_VAL=100

INT=50

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
    echo "$INT is within $MIN_VAL to $MAX_VAL."
  else
    echo "$INT is out of range."
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi

上面例子中,&&用来连接两个判断条件:大于等于$MIN_VAL,并且小于等于$MAX_VAL

使用否定操作符!时,最好用圆括号确定转义的范围。

if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
    echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
    echo "$INT is in range."
fi

上面例子中,test命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。

使用-a连接两个判断条件不太直观,一般推荐使用&&代替,上面的脚本可以改写成下面这样。

if !([ $INT -ge $MIN_VAL ] && [ $INT -le $MAX_VAL ]); then
  echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
  echo "$INT is in range."
fi

算术判断

Bash 还提供了((...))作为算术条件,进行算术运算的判断。

if ((3 > 2)); then
  echo "true"
fi

上面代码执行后,会打印出true

注意,算术判断不需要使用test命令,而是直接使用((...))结构。这个结构的返回值,决定了判断的真伪。

如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。

$ if ((1)); then echo "It is true."; fi
It is true.
$ if ((0)); then echo "It is true."; else echo "it is false."; fi
It is false.

上面例子中,((1))表示判断成立,((0))表示判断不成立。

算术条件((...))也可以用于变量赋值。

$ if (( foo = 5 ));then echo "foo is $foo"; fi
foo is 5

上面例子中,(( foo = 5 ))完成了两件事情。首先把5赋值给变量foo,然后根据返回值5,判断条件为真。

注意,赋值语句返回等号右边的值,如果返回的是,则判断为假。

$ if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi
It is false.

下面是用算术条件改写的数值判断脚本。

#!/bin/bash

INT=-5

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if ((INT == 0)); then
    echo "INT is zero."
  else
    if ((INT < 0)); then
      echo "INT is negative."
    else
      echo "INT is positive."
    fi
    if (( ((INT % 2)) == 0)); then
      echo "INT is even."
    else
      echo "INT is odd."
    fi
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi

只要是算术表达式,都能用于((...))语法,详见《Bash 的算术运算》一章。

普通命令的逻辑运算

如果if结构使用的不是test命令,而是普通命令,比如上一节的((...))算术运算,或者test命令与普通命令混用,那么可以使用 Bash 的命令控制操作符&&(AND)和||(OR),进行多个命令的逻辑运算。

$ command1 && command2
$ command1 || command2

对于&&操作符,先执行command1,只有command1执行成功后, 才会执行command2。对于||操作符,先执行command1,只有command1执行失败后, 才会执行command2

$ mkdir temp && cd temp

上面的命令会创建一个名为temp的目录,执行成功后,才会执行第二个命令,进入这个目录。

$ [ -d temp ] || mkdir temp

上面的命令会测试目录temp是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。

[ ! -d temp ] && exit 1

上面的命令中,如果temp子目录不存在,脚本会终止,并且返回值为1

下面就是if&&结合使用的写法。

if [ condition ] && [ condition ]; then
  command
fi

下面是一个示例。

#! /bin/bash

filename=$1
word1=$2
word2=$3

if grep $word1 $filename && grep $word2 $filename
then
  echo "$word1 and $word2 are both in $filename."
fi

上面的例子只有在指定文件里面,同时存在搜索词word1word2,就会执行if的命令部分。

下面的示例演示如何将一个&&判断表达式,改写成对应的if结构。

[[ -d "$dir_name" ]] && cd "$dir_name" && rm *

# 等同于

if [[ ! -d "$dir_name" ]]; then
  echo "No such directory: '$dir_name'" >&2
  exit 1
fi
if ! cd "$dir_name"; then
  echo "Cannot cd to '$dir_name'" >&2
  exit 1
fi
if ! rm *; then
  echo "File deletion failed. Check results" >&2
  exit 1
fi

case 结构

case结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elifif结构等价,但是语义更好。它的语法如下。

case expression in
  pattern )
    commands ;;
  pattern )
    commands ;;
  ...
esac

上面代码中,expression是一个表达式,pattern是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;)结尾。

#!/bin/bash

echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
  1 ) echo 1
    ;;
  2 ) echo 2
    ;;
  3 ) echo 3
    ;;
  * ) echo 输入不符合要求
esac

上面例子中,最后一条匹配语句的模式是*,这个通配符可以匹配其他字符和没有输入字符的情况,类似ifelse部分。

下面是另一个例子。

#!/bin/bash

OS=$(uname -s)

case "$OS" in
  FreeBSD) echo "This is FreeBSD" ;;
  Darwin) echo "This is Mac OSX" ;;
  AIX) echo "This is AIX" ;;
  Minix) echo "This is Minix" ;;
  Linux) echo "This is Linux" ;;
  *) echo "Failed to identify this OS" ;;
esac

上面的例子判断当前是什么操作系统。

case的匹配模式可以使用各种通配符,下面是一些例子。

  • a):匹配a
  • a|b):匹配ab
  • [[:alpha:]]):匹配单个字母。
  • ???):匹配3个字符的单词。
  • *.txt):匹配.txt结尾。
  • *):匹配任意输入,通常作为case结构的最后一个模式。
#!/bin/bash

echo -n "输入一个字母或数字 > "
read character
case $character in
  [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
                              ;;
  [0-9] )                     echo "输入了数字 $character"
                              ;;
  * )                         echo "输入不符合要求"
esac

上面例子中,使用通配符[[:lower:]] | [[:upper:]]匹配字母,[0-9]匹配数字。

Bash 4.0之前,case结构只能匹配一个条件,然后就会退出case结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&终止每个条件块。

#!/bin/bash
# test.sh

read -n 1 -p "Type a character > "
echo
case $REPLY in
  [[:upper:]])    echo "'$REPLY' is upper case." ;;&
  [[:lower:]])    echo "'$REPLY' is lower case." ;;&
  [[:alpha:]])    echo "'$REPLY' is alphabetic." ;;&
  [[:digit:]])    echo "'$REPLY' is a digit." ;;&
  [[:graph:]])    echo "'$REPLY' is a visible character." ;;&
  [[:punct:]])    echo "'$REPLY' is a punctuation symbol." ;;&
  [[:space:]])    echo "'$REPLY' is a whitespace character." ;;&
  [[:xdigit:]])   echo "'$REPLY' is a hexadecimal digit." ;;&
esac

执行上面的脚本,会得到下面的结果。

$ test.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

可以看到条件语句结尾添加了;;&以后,在匹配一个条件之后,并没有退出case结构,而是继续判断下一个条件。

参考链接

References

SmokePing 搭建及多节点探测

linuxserver 版 docker

---
services:
  smokeping:
    image: lscr.io/linuxserver/smokeping:latest
    container_name: smokeping
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - MASTER_URL=http://<master-host-ip>:80/smokeping/ #optional
      - SHARED_SECRET=password #optional
      - CACHE_DIR=/tmp #optional
    volumes:
      - ./data/config:/config
      - ./data/data:/data
    ports:
      - 80:80
    restart: unless-stopped

其中的 data/config/Targets 为监控目标配置。

配置说明

General

自行配置。

*** General ***

owner    = frytea.com
contact  = songtianlun@frytea.com
mailhost = my.mail.host
# NOTE: do not put the Image Cache below cgi-bin
# since all files under cgi-bin will be executed ... this is not
# good for images.
cgiurl   = http://localhost/smokeping/smokeping.cgi
# specify this to get syslog logging
syslogfacility = local0
# each probe is now run in its own process
# disable this to revert to the old behaviour
# concurrentprobes = no
display_name = Frytea's SmokePing

@include /config/pathnames

Probes

配置侦测频率。

** Probes ***

+ FPing
binary = /usr/sbin/fping

+ FPing6
binary = /usr/sbin/fping
protocol = 6

+ DNS
binary = /usr/bin/dig
lookup = baidu.com
pings = 5
step = 300

+ TCPPing
binary = /usr/bin/tcpping
forks = 10
offset = random
pings = 5
port = 80

Targets

配置探测目标,详见部署实例。

Slaves

多节点侦测需配置。

*** Slaves ***
secrets=/etc/smokeping/smokeping_secrets
#+boomer
#display_name=boomer
#color=0000ff

#+slave2
#display_name=another
#color=00ff00

~     

smokeping_secrets

多节点侦测时配置通信密钥,需填入兩個 smokeping 之間的密碼(兩邊密碼需相同)

host1:mysecret
host2:yoursecret
boomer:lkasdf93uhhfdfddf
~                          

多节点架设

多节点暂未实践,仅做记录,有空整理。

Slave 节点类似的方式启动,需要调整其中的参数。

---
services:
  smokeping:
    image: lscr.io/linuxserver/smokeping:latest
    container_name: smokeping
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - MASTER_URL=http://<master-host-ip>:80/smokeping/ #optional
      - SHARED_SECRET=password #optional
      - CACHE_DIR=/tmp #optional
    volumes:
      - ./data/config:/config
      - ./data/data:/data
    ports:
      - 80:80
    restart: unless-stopped

主节点 Targets 配置使用 Slave

slaves = {你第一個slave} {你第二個slave}

另外需要在 smokeping_secretsslaves 中添加關於 slave 的資訊

Slave 节点检查探测是否传递到主节点

docker logs smokeping-slave
...
Sent data to Server. Server said OK

部署实例

三大运营商

+ Other
menu = 三大网络监控
title = 监控统计

++ CT

menu = 电信网络监控
title = 电信网络监控列表
host = /Other/CT/CT-BJ /Other/CT/CT-TJ /Other/CT/CT-HLJ /Other/CT/CT-SH /Other/CT/CT-SC /Other/CT/CT-GZ

+++ CT-BJ

menu = 北京电信
title = 北京电信
alerts = someloss
host = 202.96.199.133

+++ CT-TJ

menu = 天津电信
title = 天津电信
alerts = someloss
host = 219.150.32.132

+++ CT-HLJ

menu = 黑龙江电信
title = 黑龙江电信
alerts = someloss
host = 219.147.198.242

+++ CT-SH

menu = 上海电信
title = 上海电信
alerts = someloss
host = 116.228.111.118

+++ CT-SC

menu = 四川电信
title = 四川电信
alerts = someloss
host = 61.139.2.69

+++ CT-GZ

menu = 广东电信
title = 广东电信
alerts = someloss
host = 113.111.211.22

++ CU

menu = 联通网络监控
title = 联通网络监控列表
host = /Other/CU/CU-BJ /Other/CU/CU-TJ /Other/CU/CU-HLJ /Other/CU/CU-SH /Other/CU/CU-SC /Other/CU/CU-GZ

+++ CU-BJ

menu = 北京联通
title = 北京联通
alerts = someloss
host = 61.135.169.121

+++ CU-TJ

menu = 天津联通
title = 天津联通
alerts = someloss
host = 202.99.96.68

+++ CU-HLJ

menu = 黑龙江联通
title = 黑龙江联通
alerts = someloss
host = 202.97.224.69

+++ CU-SH

menu = 上海联通
title = 上海联通
alerts = someloss
host = 210.22.84.3

+++ CU-SC

menu = 四川联通
title = 四川联通
alerts = someloss
host = 119.6.6.6

+++ CU-GZ

menu = 广东联通
title = 广东联通
alerts = someloss
host = 221.5.88.88

++ CMCC

menu = 移动网络监控
title = 移动网络监控列表
host = /Other/CMCC/CMCC-BJ /Other/CMCC/CMCC-TJ /Other/CMCC/CMCC-HLJ /Other/CMCC/CMCC-SH /Other/CMCC/CMCC-SC /Other/CMCC/CMCC-GZ

+++ CMCC-BJ

menu = 北京移动
title = 北京移动
alerts = someloss
host = 221.130.33.52

+++ CMCC-TJ

menu = 天津移动
title = 天津移动
alerts = someloss
host = 211.137.160.5 

+++ CMCC-HLJ

menu = 黑龙江移动
title = 黑龙江移动
alerts = someloss
host = 211.137.241.35

+++ CMCC-SH

menu = 上海移动
title = 上海移动
alerts = someloss
host = 117.131.19.23

+++ CMCC-SC

menu = 四川移动
title = 四川移动
alerts = someloss
host = 218.201.4.3

+++ CMCC-GZ

menu = 广东移动
title = 广东移动
alerts = someloss
host = 211.136.192.6

国际网站

+ InternetSites

menu = Internet Sites
title = Internet Sites

++ Facebook
menu = Facebook
title = Facebook
host = facebook.com

++ Youtube
menu = YouTube
title = YouTube
host = youtube.com

++ JupiterBroadcasting
menu = JupiterBroadcasting
title = JupiterBroadcasting
host = jupiterbroadcasting.com

++ GoogleSearch
menu = Google
title = google.com
host = google.com

++ GoogleSearchIpv6
menu = Google
probe = FPing6
title = ipv6.google.com
host = ipv6.google.com

++ linuxserverio
menu = linuxserver.io
title = linuxserver.io
host = linuxserver.io

欧美节点

+ Europe

menu = Europe
title = European Connectivity

++ Germany

menu = Germany
title = The Fatherland

+++ TelefonicaDE

menu = Telefonica DE
title = Telefonica DE
host = www.telefonica.de

++ Switzerland

menu = Switzerland
title = Switzerland

+++ CernIXP

menu = CernIXP
title = Cern Internet eXchange Point
host = cixp.web.cern.ch

+++ SBB

menu = SBB
title = SBB
host = www.sbb.ch/en

++ UK

menu = United Kingdom
title = United Kingdom

+++ CambridgeUni

menu = Cambridge
title = Cambridge
host = cam.ac.uk

+++ UEA

menu = UEA
title = UEA
host = www.uea.ac.uk

+ USA

menu = North America
title = North American Connectivity

++ MIT

menu = MIT
title = Massachusetts Institute of Technology Webserver
host = web.mit.edu

++ IU

menu = IU
title = Indiana University
host = www.indiana.edu

++ UCB

menu = U. C. Berkeley
title = U. C. Berkeley Webserver
host = www.berkeley.edu

++ UCSD

menu = U. C. San Diego
title = U. C. San Diego Webserver
host = ucsd.edu

++ UMN

menu =  University of Minnesota
title = University of Minnesota
host = twin-cities.umn.edu

++ OSUOSL

menu = Oregon State University Open Source Lab
title = Oregon State University Open Source Lab
host = osuosl.org

一些自建的 SmokePing

References