http://linux.chinaunix.net/bbs/viewthread.php?tid=1028015&pid=6724014&page=1&extra=page%3D1#pid6724014 原址

实在不知道该给这个东东起什么名字,但是想了想,跟主机通讯有关,而且采用了多线程机制,能高效点,就结合了我的QQ昵称,称之为
CME (cute messenger的意思)吧
修改历史
###############################
2008-08-28 07:32【cme_scanner_v1.1.rar】: 参考Zer4tul 建议,cme.sh中"#! /usr/local/bin/expect" 改为 "#! /bin/env expect",原因,每个人机器上expect的安装目录可能不一致,为了程序的兼容性更好,也就是在安装时不需要修改cme.sh,进行此修改。
################################
附件是完整的代码,rar格式的,VS打开DSP文件,或者linux下直接make就行(大家顶啊^_^)

序言
    之前看到有好几个人发了用expect实现ssh,FTP自动登陆时遇到问题的帖子,自己也想搞明白这是怎么做的,可是一直身懒都没去涉及,
不幸的是(^_^),两周前项目中遇到一个类似问题,就花了几天时间匆匆完成了任务。
这几天闲了点,把与业务无关的部分剔除了,自己另做了个程序,比较通用,简单。主要功能就是:
利用expect脚本实现在远程服务器上执行命令,而cute messenger之所以加上了cute,因为在这里实现了用多线程对命令的
并发执行过程的封装,速度比单线程要快很多。
此贴的目的:

1):希望提供这个工具能帮助改进某些朋友的工作,如果可能的话,呵呵。
(2):其次,作为C语言多线程程序设计的一个应用例子吧,其实更细致点说,也是expect自动化程序在ssh服务上的一个应用实例。
(3):能够收获更多人有益的建议,对它进行改进。
(4):能提高本版的人气,众人拾柴火焰高嘛

初识SSH的惊喜与遗憾
从05年到现在三年使用ssh这个工具,我对它的几乎一无所知,就知道用ssh登录Linux系统执行点命令,或者直接窗口拖动传文件。直到前不久的一 天,闲来无事,在linux下随便看看东西,man了一下ssh,发现有个command字眼,仔细看了下,嚄,原来ssh支持登录时传递命令阿。比如用

[Copy to clipboard] [ - ]CODE:ssh [email]root@192.168.1.100[/email] “date”
就能查看192.168.1.100上的日期了。
呵呵,这个功能确实让我欢喜了一阵子,可是后来,又发现ssh有个缺点,就是不能自动登录,每次都要手动输入密码,太烦人了。加上前一段时间项目里经常涉及到好多台机器的相同操作,我越发觉得这个限制很烦人。于是便想搞个SSH自动登录。就在baidu上面找。
发现的大多数帖子都说要给目的机器拷贝一个密钥文件,这样以后就不需要输入密码了。
但是又觉得不对劲没,如果从A访问B时不需要输入密码,那么B机器岂不是很不安全,如果有人用ROOT登录了A机器,岂不是就跟登录了B机器一样,因为虽 然他并不知道登陆B机器的密码,但是以前那个密钥文件却已经埋下了隐患。这个时候,恍然明白,原来我想要得并不是不需要输入密码的功能,而是能自动输入密 码的功能。另外,还有一个问题,即时对其它Linux机器的访问不要密码,但是要实现自动登录(或者无密码登录),第一次总要手动拷贝密钥文件到目的机 器,如果有2000台目标机器,岂不是会累死。所以我放弃了这种方法的尝试。
expect又带来希望
为了实现ssh自动登录功能,我又开始在网络上搜寻,在Q群里问了下,有人说用expect脚本可以做,便在baidu搜索expect脚本,真是兴奋 阿,在CU某人的博客里找到了一个用epect实现的ssh自动登录的例子,其实就是一段代码,用expect程序来解释。
下面是一个自动登录制定主机并执行用户命令的例子:

[Copy to clipboard] [ - ]CODE:########### auto_login.sh   #####################
1 #!/usr/local/bin/expect
2 set PASSWD [lindex $argv 1]
3 set IP     [lindex $argv 0]
4 set CMD [lindex $argv 2]
5 spawn ssh $IP $CMD
6 expect "(yes/no)?" {
7 send "yes\r"
8 expect "password:"
9 send "$PASSWD\r"
10 } "password:" {send "$PASSWD\r"} "*host " {exit 1}
11 expect eof
我没有对expect的语法进行很详细的研究,只是大概理解了这段代码,下面根据自己的理解说下它的意思:
第一行制定使用/usr/local/bin目录下的expect命令对后面的程序进行解释。
第二行,三行,四行,分别从命令行参数中获取要登录的主机IP地址,登陆密码,以及要执行的命令。
第五行,大概就是要触发这样一个事件,执行ssh $IP $CMD命令。
第6行道第11行就是expect的整个交互过程了。
如果读取到(yes/no)?提示符,就输入yes并回车,如果读取到password:提示输入密码的字符串,就输入用户登录密码(root用户)。
当然如果不是第一次登陆,以前已经登录过的话,当输入ssh $IP $CMD回车后,会直接提示输入密码也就是说会读到字符串”* password:”,这个时候会输入密码回车(send "$PASSWD\r".
另外,如果主机不可达的话,(yes/no)?和”password:”的可能都不会出现,系统会提示:
“No route to host”这个时候,我们退出程序。
所以,如果你想查看192.168.1.100上的日期的话,并不需要直接登录,而只需要执行命令:

[Copy to clipboard] [ - ]CODE:./auto_login.sh 192.168.1.100 password “date”
就能看到结果了。
其实你可以用该方法来执行任何命令。
好了,我们现在可以稍微做点复杂的应用了,比如你有10台机器,想看看这10台机器上/tmp目录有多大了,决定是否要删除它们。
假设这10台机器的IP地址是以点分式存储在一个文件ip.conf中的,每行一个地址,而登录它们的root密码都是相同的,为123456,那么你就可以做这样一个脚本来完成你的任务:

[Copy to clipboard] [ - ]CODE:############### single_thread_auto_run.sh   ############
#!/bin/sh
cat ip.conf | while read ip
do
echo “####### $ip #########” >> result.txt
/usr/local/bin/expect auto_login.sh $ip 123456 “du –sh /tmp” >> result.txt
echo “Running command on $ip over”
done
如果没有什么问题的话,对于10台机器也就是1分钟左右的时间或者四五十秒的时间就能够执行结束,而且结果会存储在result.txt文件中。
但是,现实情况并不像实验中的那么简单,现在大型企业的服务器或者Linux主机动不动几百台,或者动辄上千台,如果将上面的脚本应用到一个2000台主 机的任务当中去,我测试过,通过auto_login.sh执行一个主机大概需要3-5秒的时间,这样理论上讲,对于2000台机器,在正常联通情况下, 要串行执行完所有任务,也就是说在每台机器上完成这个任务,大概需要6000秒到1万秒的时间,大概为1小时40分钟到2小时40分钟的时间,这对于一个 普通的网管人员或者上层管理人员来说肯定是不能忍受的。看来,expect是提供了曙光,可是并不能完全解决问题啊。继续郁闷^_^

多线程加速
在第三节的最后,我们落脚到了性能的瓶颈问题上,由于数量引起的性能下降是不可避免的,所以我们尝试寻找一种方法解决此问题,也就是提高性能。可能有的人 立即会想到为何不启动多个single_thread_auto_run.sh程序?这个是一个不错的建议,可是启动多少个才算合适呢?而且这样也需要为 每个进程分配制定的IP地址个数,而且每个进程执行的结果是独立的,进程数目少了执行慢,进程数目多了又需要把IP地址分成更多的份数,将来的结果又很分 散,还需要手动合并或者又需要写程序去合并,这么做又会增加一些额外的难度和工作。
如果能让程序自动分配IP地址,并自己汇总执行结果,而且能执行更快的话,那更好了。
这个时候,我想到了用C实现的多线程程序来完成这个任务。
CME的设计与实现

CME的功能和原理其实很简单,它的目的就是实现高效的执行我们的shell脚本想要实现的功能,原理也没有多复杂,下面我大概描述大致的工作过程。
首先,程序从命令行读取IP地址的来源文件,登录密码和命令字符串。
接着,从数据文件中读取IP地址列表填充到数据结构中,也就是设备队列中。
下来,程序创建线程池对象。
然后,将设备队列中的所有IP地址对应的设备信息加入到线程队列中,平均分配给每个线程。
分配任务完成后,主线程等待线程池中工作线程执行结束。
线程池的工作线程都已经结束时,主线程遍历线程队列,打印每个线程执行的结果。
最后,退出程序。

数据结构
下面列出并介绍下程序中用到的5个结构体:
第一 struct config_t
{
    char data_file[FILE_NAME_LEN];//存储IP列表的文件

    char password[PASS_LEN];//存储密码

    char command[CMD_LEN];//存储命令

    int   dev_size;//设备数量

};
用来存储整个程序的配置信息。

第二 ,IP地址对应设备的抽象
struct dev_t
{
    unsigned long addr; //ip 地址

    int valid; //有效标志位

    int retur_value; //返回值类型

    char result_file[FILE_NAME_LEN];//存储返回结果的文件

    int id; 设备ID
};

第三,线程体的参数结构体
struct thread_param_t
{
    struct dev_t dev_list[MAX_THREAD_DEV];//该线程要处理的IP地址列表

    int dev_num;//IP地址的个数

    int valid;//参数有效性

};


第四,线程结构体,为了使程序不依赖于平台,采用了linux和windows通用的的结构定义
线程的创建也是linux和windows都支持的。
struct thread_t
{
#ifdef __LINUX__
    pthread_t thread_id;//线程标识符

#endif
#ifdef _WIN32
     DWORD thread_id;
#endif
    int thread_index; //线程编号

    enum thread_status_t status;//线程的执行状态

    struct thread_param_t parameter; //线程参数

    char result_file[FILE_NAME_LEN];//结果文件名

    int sunccess_num;//执行成功的数目

    int fail_num;//执行失败的数目

};


[ 本帖最后由 duanjigang 于 2008-8-28 07:35 编辑 ]


2008-8-27 21:06
   下载次数: 3 cme_scanner.rar (8.28 KB)2008-8-28 07:35
   下载次数: 0 cme_scanner_v1.1.rar (8.27 KB)

第五:线程池的概念
struct thread_slot_t
{
    struct     thread_t thread_list[MAX_THREADS];//线程队列

    int      thread_num_run;//本意为运行的线程数,可在本程序中却没有用到,没有意义

};

关键代码片段
下面把比较重要和需要注意的代码片段列出来进行讲述。
第一:线程池创建,调用系统相关的线程创建方法来创建线程组,个数为50个或者在头文件中修改定义,对于这段代码,在windows下和linux下都是 能编译通过并运行的,本程序唯一不能在windows上执行指出就是自动登录脚本的问题,因为我还没有尝试在windows上实现这个脚本,呵呵,没时间 搭建windows上的expect环境。
第二:由于多线程并发引起的频繁连接以及关闭会导致系统迅速出现很多22端口的状态为TIME_WAIT的TCP连接,当数目达到较高时,ssh连接就会失败,所以需要修改系统设置,这里采用如下命令

[Copy to clipboard] [ - ]CODE:/sbin/sysctl -w .net.ipv4.tcp_max_syn_backlog=4096 > /dev/null
第三,是多线程任务分配方法
在这里我们采用多线程平均分配的方法,而且在分配时,每个线程分配到的总是配置文件中相邻顺序的若干个IP地址,这样方便在打印结果时,使得每个IP地址的出现顺序与配置文件中的出现顺序一致。
比如2000个IP,100个线程,每个线程20个IP,第一个线程分配IP地址为1-20,第二个为21-40,第100个分配为1981-2000.
代码片段如下:
if(cme_config.dev_size % MAX_THREADS == 0)
    {
         per = cme_config.dev_size / MAX_THREADS;
         pos = MAX_THREADS;
    }else
    {
         per = cme_config.dev_size / MAX_THREADS + 1;
         pos =   MAX_THREADS + cme_config.dev_size -   MAX_THREADS * per;
    }
    
    if(per > MAX_THREAD_DEV)
    {
        printf("too many device [max = %d ,now = %d]\n", MAX_DEV, cme_config.dev_size);
        return 0;
    }
    
    for(i = 0; i < MAX_THREADS; i++)
    {
        int j = 0;
        int max = (i < pos) ? per : (per - 1);
        for(j = 0; j < max; j++)
        {

        memcpy(&(thread_slot->thread_list[i].parameter.dev_list[j]),
            &dev_list[nRet], sizeof(struct dev_t));
         thread_slot->thread_list[i].parameter.dev_num++;
         thread_slot->thread_list[i].status = t_state_ready;
         nRet++;
        }
    }


第四,线程执行函数体
该模型比较简单,线程在自己的多个状态之间循环,该做什么事情做什么事情,如果为dead状态就退出,如果为ready状态就执行任务,然后修改为free状态。代码片段如下:
switch(pthread->status)
        {
        case t_state_over:
        case t_state_free:
            {
                SLEEP(1);
                break;
            }
        case t_state_dead:
            {
                printf("thread %d now exits..\n", pthread->thread_index);
                return 0;
            }
        case t_state_ready:
            {
                 pthread->status = t_state_running;
                 do_work_thread(pthread);
                 pthread->status = t_state_free;
                break;
            }
        case t_state_running:
            {
                break;
            }
        default:
            {

                SLEEP(1);
                break;
            }
            
        }//end of switch

        
    }//end of whil