当前位置:Linux教程 - Linux - 在 UNIX 系统发展应用程式入门

在 UNIX 系统发展应用程式入门



         王志祥

    序言:
    同学们可能或多或少在网路上抓过UNIX 或 X 视窗的应用程式,
    你是否曾想过这些程式是如何写的?或者更进一步,你是否也想自己写一个
    大家都有兴趣去用的程式?不但可出名又可训练自己的程式设计能力.
    在 DOS 之下,可能大部分的人写应用程式都没问题.可是换到 UNIX
    之後可能就因不熟悉而不会写了.本文即是以笔者上学期所写之 UNIX
    应用程式: FindMan 作为□例,期收抛砖引玉之效.


    关於 FindMan:

    FindMan 是一集合 FindMan 、ScanGirl 、CScanGirl 三项 Utility
    为一体的程式. 当初发展 FindMan 的动机是由於上机人数太多,往往为了
    找一个人而伤眼睛地用力看 rusers 的输出.於是开始写 FindMan . 而当时正逢
    HuntGirl(u8011093) 、Hunt(u7917043) 相继推出,由於正合交大大多数男生
    的需求,因此造成轰动.本程式为了顺应潮流,加入了 ScanGirl
    及 CScangirl ( ScanGirl 中文版 ). 由於处理程序类似, 因此合并成一个程式,
    而以 link(UNIX Command: ln) 连结各项程式,程式再以第一个字母判断该执行何项程序.
    FindMan 自开放使用以来,目前每个月皆被使用了 6000 次以上. 其中有一个
    有趣现象, 最初一个月, ScanGirl 的使用率是 FindMan 的两倍,後来 FindMan 逐
    渐追上 ScanGirl, 现在 FindMan : ScanGirl 的使用率是 5:1 (FindMan 5xxx 次).
    这现象似乎显示交大人已不在如以前那样喜欢和女孩子 talk 了.


    有关在 UNIX 上写应用程式的技巧和注意事项:

    在 UNIX 上写应用程式与在 DOS 下写程式不一样的地方有几点:
    1. UNIX 的 Signal 相当於 DOS 的 Interrupt.
    2. UNIX 有许多系统呼叫,可以做低阶的动作.
    3. UNIX 是多工的作业系统,我们可善用 UNIX 的这项能力.
    4. UNIX 有程序间通讯的能力, 它可做各多工程序间、甚至各工作站间的沟通.

    当然, UNIX 的功能不只这 4 点, 读者以後可以後慢慢体会.


    FindMan 的 Initial 部份:

    由於 FindMan 主程式太长(共 10 页),而且其中有些部份如不了解就不要使用,
    否则系统可能会 ...,而你的 account 就可能会 ...,(当然,此一部份我会特别注明.)
    所以,我只会附上重要部份的程式.
    以下我会列出程式片段, 与比较奇特的变数宣告. 读者们最好遇到没看
    过的命令时, 在 UNIX 里用 man 查.

    struct passwd *p;
    uid_t uid;

    uid = getuid();
    if (!(p = getpwuid(uid))) {
    printf(\"Can\"t get user name !!\\n\");
    exit(1);
    }
    strcpy(uname,p->pw_name);
    gethostname(hsnm,sizeof(hsnm));
    if ( getlogin() == NULL ) {
    printf(\"This program is avai ....... is program..... \\n\");
    exit(1);
    }

    以上此部份是取得使用者的帐号(account) 与执行 FindMan 时的工作站名称.
    并判断使用者是否真的在线上(OnLine), 以 getlogin() 判断使用者若不在线上,
    则不让不在线上的使用者执行. 此法可防止使用者使用 rsh、at、cron 等命令.
    再来是以执行档第一个英文字判断该 show 哪一种讯息、该执行哪一种程
    式(记录在一个变数里,以此变数判断). 此部份简单,不再赘述.
    接著是程式後的参数判断,程式如下:

    while ((n = getopt(argc,argv,\"af:h\")) != EOF)
    switch(n) {
    case \"a\":
    aflg++;
    break;
    case \"f\":
    fflg++;
    hostfl=optarg;
    break;
    case \"h\":
    if (aflg || fflg || lflg || Hflg)
    errflg++;
    else
    hflg++;
    break;
    default:
    errflg++;
    }
    if (errflg) {
    printf(\"\\nUsage: %s [-a] [-h] [-H] [-f FindHostName\"s FILE]\\n\",argv[0]);
    printf(\" -h : short help\\n\");
    printf(\" -H : detail help\\n\");
    exit(1);
    }
    if (hflg) {
    printf(\"\\nUsage: %s [-a] [-h] [-H] [-f FindHostName\"s FILE]\\n\",argv[0]);
    exit(0);
    }

    程式中的 getopt() 之中的 \"af:h\" 是参数的字. 其中的 \":\" 表示参数
    之後还要加的东东. 例如 -f 之後还要加 FindHostName\"s FILE, 所以 f 之後要有 \":\".
    附带一提,这里所介绍的功能,虽然 FindMan 也会 Show 出,但大部分皆为保留功能,
    并不能使用, 必须等我有空实在加上.届时我会 announce.
    这部份所用的技巧可使程式能提供使用者多样的选择.程式中的 aflg、fflg
    等皆为旗标变数. 它可记录参数的状况.
    再下来是根据变数决定程式要读取的 database(有 .look 及我的中英文
    girl database), 并记录到我的记录档里.此部份程式亦略去不提.
    接下来是 UNIX 的 Signal 之运用和子程序的建立. 当初之所以使用 fork
    建立许多子程序并使用复杂且吃 Load 的程序间通讯, 是因为如此可以家快速度.
    由於有时会有一些工作站当掉或无法回应 rusers 的 request,这些工作站即会
    延迟 rusers □集资料的时间. 我的程式即可部份解决这个问题. 由主程序 fork 出
    去的各子程序各自 rusers 一个工作站,先得到资料的程序先向主程序传送.
    (程序间传送需用程序间通讯的技巧) 如此就可不管那些不传资料的工作站了.
    所以我的 FindMan 常不按顺序 show 各工作站的资讯後,停了很久才结束便是
    这个道理. 知道原理後,下次看到讯息出完停下时,不必苦等而可以 Ctrl-C 取
    而代之. 现在由於计中的 rusers 改良过而不必再苦等.因此利用此技巧取代
    复杂且耗 Load 的程序间通讯便成为我的下一个课题.
    □话少说,看下面的程式:

    signal(SIGCLD, SIG_IGN);
    signal(SIGINT, timeout);

    for (i=0; i if ((pid = fork()) == 0) {
    pitch(hosts[i]);
    break;
    }
    }
    if (pid != 0)
    {
    tmpp[0]=(char)0;
    tmpp[1]=\"\\0\";
    catch(tmpp);
    if ((short)tmpp[0]) {
    printf(\"There are %d workstation responding fail...\\n\",tmpp[0]);
    printf(\" Fail workstation: %s\\n\",&tmpp[1]);
    }

    前两个 Signal,第一个的意义是忽略子程序结束的讯号.而第二个的意义是
    遇到 Ctrl-C 之类的中断时要执行 timeout() 函数.而底下则是用 fork() 产生
    子程序,有关 fork() 的意义与用法,请自行参考资料. 程式中的 catch() 是我定义
    的函数,下次再说.而 tmpp[0] 则是用来判断工作站是否有 respond.



    结语:

    由於篇幅及时间因素,本文在此告一段落 ,下期再继续.若有任何建议,欢迎用
    E-Mail 寄到 u7911093@cc.nctu.edu.tw .


    在 UNIX 系统发展应用程式入门 (二) 王志祥




    取得 User 资料的 pitch 函数:
    上其的 FindMan 的 Initial 部份提到两个 User Define 的函数,
    一个是 catch, 另一个即是 pitch. Initial 部份 fork 的二十个程序都呼叫
    pitch 函数向二十台工作站收集 User login 的资料并筛选出使用者所需的
    资料, 先整理出来的程序便先向主程序要求传送资料, 传送以 Socket 完成.
    有关 Socket 的部份以後再谈. 以下是 pitch 函数的部份程式:

    pitch(host)
    char *host;
    {
    int bufsize;
    int ps, j, k, l;
    char buffer[BUFSIZE];
    int count;
    struct utmpidlearr up;
    int tmp, gcount, gidx[25];
    char gname[25][80];
    char line[25][9];
    char name[25][9];
    char hhost[25][17];
    long time[25];
    unsigned idle[25];
    char temp[80];

    up.uia_arr=(struct utmpidle **) malloc(25*sizeof(struct utmpidle *));

    : : :
    : : :
    /* 此处省略要求 Connect 的 Socket 之 Initial 部份. */
    : : :
    : : :


    strcpy(buffer,host);
    k=strlen(host);
    strcpy(&buffer[k],\":\\n\");
    k+=2;
    if ( rusers(host, &up) == 0 ) {
    count = up.uia_cnt;
    buffer[k++] = (char) count;
    for ( j=0; j strncpy(line[j],up.uia_arr[j]->ui_utmp.ut_line,8);
    line[j][8]=\"\\0\";
    strncpy(name[j],up.uia_arr[j]->ui_utmp.ut_name,8);
    name[j][8]=\"\\0\";
    strncpy(hhost[j],up.uia_arr[j]->ui_utmp.ut_host,16);
    hhost[j][16]=\"\\0\";
    time[j]=up.uia_arr[j]->ui_utmp.ut_time;
    idle[j]=up.uia_arr[j]->ui_idle;
    }
    if (count != 0) {
    gcount=check(count,name,gname,gidx);
    buffer[k++] = (char) gcount;
    if (gcount !=0)
    for (j=0; j strcpy(&buffer[k],gname[j]);
    k+=strlen(gname[j]);
    do {
    tmp=gidx[j++];
    if ( j > gcount )
    break;

    buffer[k++]=\"\\t\";
    strcpy(&buffer[k],line[tmp]);
    k+=strlen(line[tmp]);

    buffer[k++]=\"\\t\";
    if ((l=idle[tmp]/60) != 0)
    sprintf(temp,\"%d:%d\\t\",l,(idle[tmp]%60));
    else
    sprintf(temp,\"%d\\t\",(idle[tmp]%60));
    strcpy(&buffer[k],temp);
    k+=strlen(temp);


    strcpy(temp,asctime(localtime(&time[tmp])));
    strcpy(&buffer[k],temp);
    k+=(strlen(temp) - 6);

    buffer[k++]=\"\\t\";
    strcpy(&buffer[k],hhost[tmp]);
    k+=strlen(hhost[tmp]);

    buffer[k++]=\"\\n\";
    } while (strcmp(name[tmp],\"repeat\") == 0);
    }
    }
    buffer[k++]=\"\\0\";
    }
    else {
    count = -1;
    buffer[k++] = (char) count;
    }

    : : :
    : : :
    /* 此处省略要求 Connect 的 Socket 之传送资料部份. */
    : : :
    : : :

    }

    其中 rusers 的部份可在 CCSUN 用 \" man 3 rusers \" 查, 兹摘录
    如下:

    ==================> man 3 rusers 的摘录 <==============
    NAME
    rnusers, rusers - return information about users on remote
    machines

    DESCRIPTION
    rnusers() returns the number of users logged on to host (-1
    if it cannot determine that number). rusers() fills the
    utmpidlearr structure with data about host, and returns 0 if
    successful.

    PROGRAMMING
    #include <=== 查 struct 的地方
    rnusers(host)
    char *host
    rusers(host, up)
    char *host
    struct utmpidlearr *up;
    ==========================================================

    由於以上的 manual 中并没有指出 utmpidlearr 的结构型态如何,
    因此我们必须列出我们所需要的 head file 资料以取得我们要的
    utmpidlearr 结构. 以下是我的搜寻所得:

    ==================> utmpidlearr 重要资料 <=================
    struct utmpidle {
    struct utmp ui_utmp;
    unsigned ui_idle;
    };

    struct utmpidlearr {
    struct utmpidle **uia_arr;
    int uia_cnt;
    };

    struct utmp {
    char ut_line[8]; /* tty name */
    char ut_name[8]; /* user id */
    char ut_host[16]; /* host name, if remote */
    long ut_time; /* time on */
    };
    ============================================================

    有了这些资料以後, 再看前面的程式就比较易懂了. ( 若因程式的
    变数命名太差, 或流程太混乱, 请见谅. 因这一部份在完成之初又不断加新
    的功能, 而不是一次完成的. )

    程式中的 buffer 这个变数储存欲送出的资料, 其格式为:
    ==================================================================
    工作站名 login 人数 符合资料的人数 帐号及其上机状态
    any bytes 1 bytes 1 bytes any bytes
    ==================================================================

    以下面 FindMan 的输出为例说明:
    =====================> FindMan 部份输出 <=====================
    ccsun7:
    u813xx07 -----> xxxxxxxxxxx
    ttyq0 0 Tue Apr 27 23:56:53 xxxuling.Dorm10.
    ttyp1 0 Wed Apr 28 01:46:29 xxxuling.Dorm10.

    u802xx23 Chen2 xxxxxxxx
    ttyr6 31:47 Mon Apr 26 18:47:17 140.113.1xx.177:

    ccsun16:
    u813xx16 -----> xxxxxxxxxxx
    ttyp1 0 Wed Apr 28 01:46:29 xxxmy.DormChu.NC
    ==============================================================

    其中工作站名不限 byte 数, 接收端以 \":\\n\" 判断此部份的资料
    结束与否. 当第二部份为 0 时即填入 -1 表示无人, 与资料比对後找不到
    人亦如此. check 函数即是处理与资料比对的工作, FindMan 是与 .look
    比对, ScanGirl 与 girl 的资料档比对. check 的函数传回 gcount、
    gname、gidx 三个变数. gcount 即是 match 的人数, gname 是从 rusers
    传回的资料档稍作修改 ( 重复的帐号改成 \"repeat\" ) 而成. 而 gidx 是
    rusers 传回资料的排序後的 index.
    从 check 函数传回的 gname、gidx 构成 buffer 阵列最後的资料.
    即 \"帐号及其上机状态\" 部份. 全部完成後即将资料送出. 由於 check 函数
    并不难写, 故不列出.

    由於篇幅及时间所限, 这次只讲 pitch 函数, 其它以後再继续,
    下期见!! 8-)

    —————————————————————————————————
    FindMan 关闭启示:
    近来 CCSUN 的使用率大幅增加 , 使得 CCSUN
    的 Load 高居不下, 我的 FindMan 公用程式只有在 Load 很轻的
    SUN 上测试过 ( 0 到 2 ), 没想到最近 Load 重的 SUN ( 7 以上 )
    上执行时, 我程式中加快搜寻速度的部份竟成为致命伤. 由於一
    次开二十个程序, 若不幸因电脑异常, 则大部分程序会停住而无法离开,
    使电脑 Load 更形加重, 也令系统管理者困扰. 在我花了十数小时不断
    改程式後( 包括加上 Alarm Signal 控制执行时间 ), 终於还是不能克服
    Load 重时的异常状况. 看来, 我的程式理念 ( 以多个程序平行处理来增
    快速度 ) 只能在人数少的工作站或特殊的电脑才能用. 有志於发展应用程
    式的同学, 切记这个教训.

    由於旧的 FindMan 程式有时会出状况, 我已经将它去掉,
    以後有空时再写新的单一程序之 FindMan. 虽然程式已撤掉, 但本文可当
    成纯为讨论程式设计, 故仍继续下去.
    发布人:netbull 来自:嵌入式Linux