UnixLinux 编程实践学习

本贴最后更新于 435 天前,其中的信息可能已经物是人非

目录:

2 - 用户、文件操作与联机帮助1

4 - 文件系统: pwd 编写2

6 - 为用户编程:终端控制和信号8

8-进程和程序:编写命令解释器 sh17

9-可编程的 shell、shell 变量和环境:编写自己的 shell21

学习系统调用3


  1. 2 - 用户、文件操作与联机帮助

    2.1 who 命令

    • who 命令可以干啥?
    • who 该如何使用?
    • 如何自己编写一个 who?
      • open 命令
      • read 命令

    who 命令可以干啥?

    who 命令用来显示系统用户的信息

    who 是如何工作的?

    1. 从 Unix 中学习 Unix
      • 阅读联机帮助
      • 搜索联机帮助
      • 阅读.h 文件
      • 从阅读部分得到启示
    2. 阅读联机帮助
    • 可以用 man who 命令查看(在 WSL2 上 man 命令不好用)

      注意:

    • 根据 man 的信息,who 命令会查询 /var/run/utmp 的信息

    1. 搜索连接帮助
    • 根据 man -k utmp ,可以查看关于 utmp 的信息

    • 注意 utmp(5) - login records (记载了登入信息)

      • 5 是小节编号,说明该帮助位于第 5 节
    • 输入 man 5 utmp,查看 utmp 的帮助

      可以看到其保存在 utmp.h 文件中

    1. 阅读.h 文件
    • 通常.h 文件包含在 /usr/include 中,我们直接进入/usr/include 中查看 utmp.h 文件即可
      • 发现其又包含了一次.h 文件

      • 再进入查看,可以找到 utmp 结构体,其内部保存了用户数据

    1. who 的工作原理
      1. 打开 utmp
      2. 读取 utmp 结构体。 依据搜索连接帮助中 man 5 utmp ,可以得知想要直到 utmp 结构体内部信息,必须先打开/var/run/utmp 文件
      3. 显示记录
      4. 关闭 utmp

    自己实现 who 命令?

    #include<stdio.h>
    #include<stdlib.h>
    #include <unistd.h>
    #include <utmp.h>
    #include <fcntl.h>
    #include <time.h>
    
    #define SHOWHOST    //包括远方的输出设备
    
    void showtime(time_t);
    void show_info(struct utmp*);
    
    int main()
    {
        struct utmp current_record;
    
        int utmpfd;     //从此文件描述符中读取数据
        int reclen = sizeof(current_record);    //读取数据的大小
    
         // open函数用来打开指定目录的文件,UTMP_FILE代表了“/var/run/utmp” 
        if( (utmpfd =open(UTMP_FILE,O_RDONLY)) == -1){
            perror(UTMP_FILE);
            exit(1);
        }
    
       //把utmpfd读取到current_record中 
        while( read(utmpfd,&current_record,reclen) == reclen)
            show_info(&current_record);
    
        close(utmpfd);
        return 0;
            
    }
    
    void show_info(struct utmp* utbufp)
    {
        // if(utbufp->ut_type != USER_PROCESS)
        //     return;
        printf("% -8.8s",utbufp->ut_user);
        printf(" ");
        printf("%s",utbufp->ut_line);
        printf(" ");
        showtime(utbufp->ut_tv.tv_sec);
    
        #ifdef SHOWHOST
        if(utbufp->ut_host[0] != '\0')
            printf("(%s)",utbufp->ut_host);
        #endif
        printf("\n");
    }
    
    void showtime(time_t timeval){
        char    *cp;
        cp = ctime(&timeval);   //convert time to string
    
        printf("%s",cp);
    }
    
    
    
    

    2.2 编写 cp(读和写)

  2. 4 - 文件系统: pwd 编写

    学习系统调用1

    • stat1;
    • opendir1 , opendir1,closedirreaddir1;
    • chdir1

    文件系统介绍

    文件是存放数据的地方,而目录是文件的列表。

    文件系统的内部结构

    文件系统是对硬盘的抽象。

    一个磁盘能够存储大量数据,一个磁盘可以被划分为各区。成为分区

    一个硬盘由一些磁性盘片组成。每个盘片又可以划分为很多扇区,给每个扇区分配编号,称为块编号

    • 文件系统可以用来存储文件内容、文件属性和目录。

    • 文件系统存储空间结构如下:

    • 存储结构

      • 超级块存放文件系统本身的结构信息,比如每个区域的大小
      • i -节点存储了文件属性,如文件的大小、所有者和最近修改事件
      • 数据区存储了文件内容

    目录入口是文件名和 i-节点号组成的对。i-节点号指向磁盘上的一个结构,该结构包含文件信息和数据块的分配,如图 4.15 所示。

    文件系统的实现:创建一个文件的过程

    • 当我们调用 cat spw.c > userlist 时,文件系统的内部操作是怎么回事?

    创建一个文件的 4 个步骤:

    1. 存储属性
      内核先找到一个空的 i-节点,将文件的信息记录其中。比如找到了 47。
    2. 存储数据
      假设该文件需要三个存储磁盘,内核则从磁盘中找到 3 个自由块,比图找到了 627、200、992
    3. 记录分配情况
      把内核分配的自由块的序列号记录到 i-节点中
    4. 添加文件名到目录中
      新文件的名字叫 userlist,把入口(47,userlist)添加到目录文件中。

    编写 pwd

    • pwd 能干啥?
    • pwd 的实现原理?
    • 如何自己编写 pwd?
    • pwd 能干啥?

    pwd 可以查看从根目录到当前所在目录的路径

    • pwd 的实现原理

    从当前目录开始上溯,当前目录的名称为 "." 。先得到当前目录的 i - 节点号。上溯后,访问目录,查找节点号对应的名称。再上溯,直到目录树的顶端(即".."和"."的 i-节点号相同的时候)。

    1. 查找当前目录的 i-节点编号(通过 stat 系统调用,可以得到当前目录的状态)
    2. 上溯至父目录(chdir 系统调用 - 改变当前进程的所在目录)
    3. 在目录中,根据 i-节点编号查找目录的名称( readdir 读取目录)
    4. 打印

    但是注意到,我们需要先打印最上层,如何保证打印的顺序?

    通过递归来保证打印顺序

    #include <stdio.h>
    #include <dirent.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    
    //得到此该文件的编号
    ino_t get_inode(char *);
    //根据编号查找文件姓名
    void inum_to_name(ino_t,char*,int);
    //打印路径
    void printpathto(ino_t );
    
    int main(int argc)
    {
        if(argc != 1){
            fprintf(stderr,"Stdin错误!");
        }
    
        printpathto( get_inode(".") );
        putchar('\n');
        return 0;
    }
    
    void printpathto(ino_t this_inode)
    {
        ino_t my_node;
        char its_name[BUFSIZ];
        if(get_inode("..") != this_inode)
        {
            //切换到上级目录
            chdir("..");
            //得到上级目录的名称
            inum_to_name(this_inode,its_name,BUFSIZ);
            my_node = get_inode(".");
            printpathto(my_node);
            printf("/%s", its_name);
        }
    
    }
    
    //得到文件的编号
    ino_t get_inode(char *fname)
    {
        struct stat info;
        if( stat(fname,&info) == -1)
        {
            perror(fname);
            exit(1);
        }
          
        return info.st_ino;
    }
    
    
    //根据编号得到路径,并将其存储在namebuf中
    void inum_to_name( ino_t inode, char * namebuf, int buflen)
    {
        //先打开文件夹,遍历文件夹中与编号匹配的文件
        DIR * dir_ptr;
        struct dirent*  direntp;
        dir_ptr = opendir(".");
        if(dir_ptr == NULL){
           perror(".");
           exit(1); 
        }
        while ((direntp = readdir(dir_ptr)) != NULL)
        {
            if(direntp->d_ino == inode){
                strncpy(namebuf,direntp->d_name,buflen);
                namebuf[buflen - 1] = '\0';
                closedir(dir_ptr);
                return;
            }
        }
        fprintf(stderr,"error looking for inum %ld\n",inode);
        exit(1);  
    }
    
    
    
  3. 学习系统调用

    stat

    • 作用:获取文件的状态(比如得到文件的 i-节点号、文件的大小)

    • 文件状态定义如下:
    struct stat {
      dev_t    st_dev;    /* file system id */
      ino_t    st_ino;    /* file id */
      mode_t    st_mode;  /* ownership/protection */
      nlink_t    st_nlink;  /* number of links */
      uid_t    st_uid;    /* user id */
      gid_t    st_gid;    /* group id */
      dev_t    st_rdev;
      off_t    st_size;  /* file size in # of bytes */
      unsigned long  st_blksize;  /* block size */
      unsigned long  st_blocks;  /* file size in # of blocks */
      unsigned long  st_atime;  /* time file was last accessed */
      unsigned long  __unused1;
      unsigned long  st_mtime;  /* time file was last modified */
      unsigned long  __unused2;
      unsigned long  st_ctime;  /* time file status was last changed */
      unsigned long  __unused3;
      unsigned long  __unused4;
      unsigned long  __unused5;
    };
    
    具体含义:
    
    ![](https://secure2.wostatic.cn/static/maKiLfKWbgLxJpcCywx91Z/image.png?auth_key=1693225046-fNnQrm9mF1ogDDiB2tMufT-0-039ce0de0d3337a9df1129313ad1c5ec)
    
    • 使用实例:
    //得到文件的编号
    ino_t get_inode(char *fname)
    {
        struct stat info;
        if( stat(fname,&info) == -1)
        {
            perror(fname);
            exit(1);
        }
          
        return info.st_ino;
    }
    

    opendir , readdir,closedir

    • 作用:打开文件并读取目录,然后关闭目录
    • 实现原理
    • 返回的是结构体 dirent 指目录的当前项,dirent 存放在 DIR 数据流中。 参数是 DIR 结构体指针,代表目录,是存放目录项的数据流
    • 实例:读取一个目录,并且输出目录内的文件名
    
    //**显示文件夹信息**
    void do_ls(char dirname[])
    {
      //首先定义一个目录与目录项
        DIR* dir_ptr;
        struct dirent * direntp;
      //读取目录名称,返回DIR目录流
        if((dir_ptr = opendir(dirname)) == NULL)
            fprintf(stderr,"lsl: cannot open %s\n",dirname);
        else
        {
        //**读取目录流,得到目录项**
            while ((direntp = readdir(dir_ptr)) != NULL)
            {
                printf("%s\n",direntp->d_name);
            }
        //**关闭目录**
            closedir(dir_ptr);
        }
          
    }
    

    chdir

    • 作用:改变当前进程所在目录

    • 使用实例:
            //**切换到上级目录**
            chdir("..");
    

    • 作用:在一个程序中调用另一个程序

    • 函数原型:int execvp(const char __file, ​char​ ​const****​​ ​​*****__argv)

    • 注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。

    • 详解:

      image

    • 实例:调用 ls

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      
      
      int main()
      {
          char *arglist[3];
      
          arglist[0] = "ls";
          arglist[1] = "-l";
          arglist[2] = 0;
      
          printf("*** About to exec ls -l\n");
          execvp("ls",arglist);
          printf("*** ls is done. bye\n");
      }
      

    fork

    • fork 的意义

      我们在用 execvp 调用新的进程后,会把原来的进程给替换掉。比如说在我们自己编写的 shell 进程中调用 ls 后,shell 进程也被关闭了。

      而我们希望 execvp 调用后,还能返回到原来的 shell 进程中。

      所以我们可以通过 fork 建立一个新的进程,然后在新进程中调用 execvp,父进程调用 wait 等待新进程执行完毕,当新进程执行 exit 时,父进程就会收到信号,然后父进程继续运行

    • fork 详解

      image

    fork 的简单应用

    /*
        建立新的进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    
    int main()
    {
        int ret_from_fork,mypid;
    
        mypid = getpid();
    
        printf("Before: my pid is %d\n",mypid);
    
        ret_from_fork = fork();
    
        sleep(1);
        printf("After: my pid is %d, fork() said %d\n",
            getpid(), ret_from_fork);
    }
    

    输出:

    image

    fork 的特征(区分父进程和子进程)

    • fork 会建立一个新的进程
    • fork()函数会返回进程号,在子进程中,返回的进程号是 0;父进程中,返回的进程号是子进程的进程号

    • 实例:
    /*
        判断自己是子进程还是父进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main()
    {
        int fork_rv;
    
        printf("Before : my pid is %d\n",getpid());
    
        fork_rv = fork();       //在子进程中,fork()返回0
    
        if(fork_rv == -1)
            perror("fork");
          
        else if(fork_rv == 0)
            printf("I am the child. my pid = %d\n",getpid());
        else
            printf("I am the parent. my child is %d\n",fork_rv);
    }
    

    输出:

    image

    wait/exit

    • 作用:进程调用 wait 等待子进程结束

    • 用法:pid = wait(&status)

    • wait 详解

      image

    • 系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。

    • statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。

    image

    • 实例: waitdemo.c

      该例子显示了子进程调用 exit 是如何触发 wait 返回父进程并如何得到子进程返回时的状态 的。

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include<sys/wait.h>
    
    #define DELAY   5
    
    int main()
    {
        int newpid;
        void child_code(), parent_code();
    
        printf("before: mypid is %d\n",getpid());
    
        if( (newpid = fork()) == -1)
            perror("fork");
        else if( newpid == 0)	
            child_code(DELAY);	//执行子进程的代码
        else
            parent_code(newpid);		//父进程代码
    }
    
    //子进程执行的代码
    void child_code(int delay)
    {
        printf("child %d here . will sleep for %d seconds\n",getpid(),delay);
        sleep(delay);
        printf("child done about to exit\n");
        exit(17);
    }
    
    //父进程代码,等待子进程结束
    void parent_code(int childpid)
    {
        int wait_rv;        //return value from wait()
        int child_status;
        int high_8 , low_7, bit_7;
         
    //等待子进程结束
        wait_rv = wait(&child_status);
    
    //子进程结束后,执行以下代码
        printf("child_status: %p\n",*(&child_status) >> 8);
        printf("done waiting fot %d.&Wait returned: %d\n",childpid,wait_rv);
    
    //得到子进程退出时的状态
        high_8 = child_status >> 8;     //1111 1111 0000 0000
        low_7 = child_status &0x7F;     //0000 0000 0111 1111
        bit_7 = child_status & 0x80;    //0000 0000 1000 0000
        printf("status: exit = %d, sig = %d, core = %d\n",high_8, low_7 ,bit_7);
    }
    
    

    image

  4. stat

    • 作用:获取文件的状态(比如得到文件的 i-节点号、文件的大小)

    • 文件状态定义如下:
    struct stat {
      dev_t    st_dev;    /* file system id */
      ino_t    st_ino;    /* file id */
      mode_t    st_mode;  /* ownership/protection */
      nlink_t    st_nlink;  /* number of links */
      uid_t    st_uid;    /* user id */
      gid_t    st_gid;    /* group id */
      dev_t    st_rdev;
      off_t    st_size;  /* file size in # of bytes */
      unsigned long  st_blksize;  /* block size */
      unsigned long  st_blocks;  /* file size in # of blocks */
      unsigned long  st_atime;  /* time file was last accessed */
      unsigned long  __unused1;
      unsigned long  st_mtime;  /* time file was last modified */
      unsigned long  __unused2;
      unsigned long  st_ctime;  /* time file status was last changed */
      unsigned long  __unused3;
      unsigned long  __unused4;
      unsigned long  __unused5;
    };
    
    具体含义:
    
    ![](https://secure2.wostatic.cn/static/maKiLfKWbgLxJpcCywx91Z/image.png?auth_key=1693225046-fNnQrm9mF1ogDDiB2tMufT-0-039ce0de0d3337a9df1129313ad1c5ec)
    
    • 使用实例:
    //得到文件的编号
    ino_t get_inode(char *fname)
    {
        struct stat info;
        if( stat(fname,&info) == -1)
        {
            perror(fname);
            exit(1);
        }
          
        return info.st_ino;
    }
    
  5. opendir , readdir,closedir

    • 作用:打开文件并读取目录,然后关闭目录
    • 实现原理
    • 返回的是结构体 dirent 指目录的当前项,dirent 存放在 DIR 数据流中。 参数是 DIR 结构体指针,代表目录,是存放目录项的数据流
    • 实例:读取一个目录,并且输出目录内的文件名
    
    //**显示文件夹信息**
    void do_ls(char dirname[])
    {
      //首先定义一个目录与目录项
        DIR* dir_ptr;
        struct dirent * direntp;
      //读取目录名称,返回DIR目录流
        if((dir_ptr = opendir(dirname)) == NULL)
            fprintf(stderr,"lsl: cannot open %s\n",dirname);
        else
        {
        //**读取目录流,得到目录项**
            while ((direntp = readdir(dir_ptr)) != NULL)
            {
                printf("%s\n",direntp->d_name);
            }
        //**关闭目录**
            closedir(dir_ptr);
        }
          
    }
    
    • stat1;
    • opendir1 , opendir1,closedirreaddir1;
    • chdir1
  6. chdir

    • 作用:改变当前进程所在目录

    • 使用实例:
            //**切换到上级目录**
            chdir("..");
    

    • 作用:在一个程序中调用另一个程序

    • 函数原型:int execvp(const char __file, ​char​ ​const****​​ ​​*****__argv)

    • 注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。

    • 详解:

      image

    • 实例:调用 ls

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      
      
      int main()
      {
          char *arglist[3];
      
          arglist[0] = "ls";
          arglist[1] = "-l";
          arglist[2] = 0;
      
          printf("*** About to exec ls -l\n");
          execvp("ls",arglist);
          printf("*** ls is done. bye\n");
      }
      

    fork

    • fork 的意义

      我们在用 execvp 调用新的进程后,会把原来的进程给替换掉。比如说在我们自己编写的 shell 进程中调用 ls 后,shell 进程也被关闭了。

      而我们希望 execvp 调用后,还能返回到原来的 shell 进程中。

      所以我们可以通过 fork 建立一个新的进程,然后在新进程中调用 execvp,父进程调用 wait 等待新进程执行完毕,当新进程执行 exit 时,父进程就会收到信号,然后父进程继续运行

    • fork 详解

      image

    fork 的简单应用

    /*
        建立新的进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    
    int main()
    {
        int ret_from_fork,mypid;
    
        mypid = getpid();
    
        printf("Before: my pid is %d\n",mypid);
    
        ret_from_fork = fork();
    
        sleep(1);
        printf("After: my pid is %d, fork() said %d\n",
            getpid(), ret_from_fork);
    }
    

    输出:

    image

    fork 的特征(区分父进程和子进程)

    • fork 会建立一个新的进程
    • fork()函数会返回进程号,在子进程中,返回的进程号是 0;父进程中,返回的进程号是子进程的进程号

    • 实例:
    /*
        判断自己是子进程还是父进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main()
    {
        int fork_rv;
    
        printf("Before : my pid is %d\n",getpid());
    
        fork_rv = fork();       //在子进程中,fork()返回0
    
        if(fork_rv == -1)
            perror("fork");
          
        else if(fork_rv == 0)
            printf("I am the child. my pid = %d\n",getpid());
        else
            printf("I am the parent. my child is %d\n",fork_rv);
    }
    

    输出:

    image

    wait/exit

    • 作用:进程调用 wait 等待子进程结束

    • 用法:pid = wait(&status)

    • wait 详解

      image

    • 系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。

    • statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。

    image

    • 实例: waitdemo.c

      该例子显示了子进程调用 exit 是如何触发 wait 返回父进程并如何得到子进程返回时的状态 的。

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include<sys/wait.h>
    
    #define DELAY   5
    
    int main()
    {
        int newpid;
        void child_code(), parent_code();
    
        printf("before: mypid is %d\n",getpid());
    
        if( (newpid = fork()) == -1)
            perror("fork");
        else if( newpid == 0)	
            child_code(DELAY);	//执行子进程的代码
        else
            parent_code(newpid);		//父进程代码
    }
    
    //子进程执行的代码
    void child_code(int delay)
    {
        printf("child %d here . will sleep for %d seconds\n",getpid(),delay);
        sleep(delay);
        printf("child done about to exit\n");
        exit(17);
    }
    
    //父进程代码,等待子进程结束
    void parent_code(int childpid)
    {
        int wait_rv;        //return value from wait()
        int child_status;
        int high_8 , low_7, bit_7;
         
    //等待子进程结束
        wait_rv = wait(&child_status);
    
    //子进程结束后,执行以下代码
        printf("child_status: %p\n",*(&child_status) >> 8);
        printf("done waiting fot %d.&Wait returned: %d\n",childpid,wait_rv);
    
    //得到子进程退出时的状态
        high_8 = child_status >> 8;     //1111 1111 0000 0000
        low_7 = child_status &0x7F;     //0000 0000 0111 1111
        bit_7 = child_status & 0x80;    //0000 0000 1000 0000
        printf("status: exit = %d, sig = %d, core = %d\n",high_8, low_7 ,bit_7);
    }
    
    

    image

  7. 6 - 为用户编程:终端控制和信号

    学习系统调用1

    • tcsetattr/tcgetattr
    • fcntl
    • signal

    如何编写终端驱动?

    应用场景

    我们有时需要改变自己与终端的交互模式。比如输入密码的时候关闭屏幕的回显,以保证机密性。
    

    终端驱动程序简介

    驱动程序决定了用户和终端的交互模式。

    我们可以选择与终端的交互模式:比如关闭回显,关闭缓冲。

    image

    如何实现编写?

    思路:

    • 从驱动程序获得属性(通过系统调用 tcgetattr),属性存放在 termios 结构体中
    • 修改所要修改的属性
    • 将修改的属性送回驱动程序(通过系统调用 tcsetattr)

    举例,以下代码为一个连接开启字符回显:

    image

    编写驱动:关于位

    termios 结构体存储了决定用户与终端交互状态的位:

    image

    image

    改变位的状态即改变交互状态(比如是否开启回显,是否开启缓存)

    每个属性在标志集中都占有一位。对属性的操作如下:

    image

    实例,改变回显

    此例将键盘回显开或管。如果输入'y',则终端的回显位被开启,否则被关闭。

    #include <termios.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    # include<stdio.h>
    #include <sys/stat.h>
    /*
        如果命令行以'y'开始,终端的回显将会开启
        否则回显会被关闭
    */
    #define oops(s,x) {perror(s); exit(x);}
    
    int main(int argc,char* argv[])
    {
        //指向终端的结构体
        struct termios info;
        if(argc == 1)
            exit(0);
             
        int rv;
        rv = tcgetattr(0,&info);
        if(rv == -1){
            perror("tcgetattr");
            exit(1);
        }
        if(argv[1][0] == 'y')
            info.c_lflag |= ECHO;       //打开回显标志位
        else
            info.c_lflag &= ~ECHO;      //关闭回显标志位
          
        //设置终端属性
        if( tcsetattr(0,TCSANOW, &info) == -1)
            oops("tcsetattr",2);
          
         if(info.c_lflag & ECHO)
            printf("echo is on,since its bit is 1\n");
        else
            printf("echo is OFF,since its bit is 0\n");
    
    }
    

    如何编写一个用户程序?

    应用场景

    很多用户应用程序,例如,自动取款机和计算机游戏,都会向用户提出yes/no的问题。
    

    简易版本

    思路:

    • 对用户显示提示问题
    • 接受输入
    • 如果是'y',返回 0
    • 如果是'n',返回 1
    #include <stdio.h>
    #include <stdlib.h>
    #include <termios.h>
    
    #define QUESTION "Do you want another transaction?"
    int get_response(char* );
    
    int main()
    {
        int response;
        response = get_response(QUESTION);
        return response;
    }
    
    int get_response(char* )
    {
        //输出提问
        printf("%s (y/n)?",QUESTION);
    
        //等待用户输入
        while (1)
        {
            switch (getchar())
            {
            case 'y'  :
            case 'Y'  : return 0;
            case 'n'  : 
            case 'N'  : 
            case EOF  : return  1;
            default   : exit(1);        
            }
        }
    

    问题

    简单版本中只有用户按回车键后,程序才能接受到程序。第二,用户按回车后,程序接受整行的数据并对其进行处理。
    

    立即响应版本

    版本改进

    可以即时响应用户输入

    思路

    • 先保存驱动原有模式
    • 设置驱动模式,关闭缓存
    • 接受输入
    • 恢复原来模式

    代码实现


    • 主函数实现

    #include <stdio.h>
    #include <stdlib.h>
    #include <termios.h>

    /*
    关闭缓存,程序可以立即响应输入
    */

    #define QUESTION "Do you want another transaction?"
    int get_response(char* );
    void set_mode();
    int tty_mode(int);

    int main()
    {
    int response;
    //保存状态
    tty_mode(0);1
    //设置状态
    set_mode();1
    //接受输入
    response = get_response^11;
    //恢复出厂设置
    tty_mode(0);^9;
    return response;
    }

    保存出厂设置与恢复出厂设置(==**int tty_mode(int);**==​)

    思路

    设置一个静态变量用来保存原有的驱动状态

    //读取状态,保存状态
    int tty_mode(int how)
    {
        static struct termios origin_mode;
        if(how == 0)
            tcgetattr(0,&origin_mode);
        else
            return tcsetattr(0,TCSANOW,&origin_mode); 
    }
    

    设置驱动模式,关闭缓存(**==set_mode();==**​​​​​)

    思路

    • 关闭缓存标志位
    void set_mode()
    {
        struct termios ttystate;
        tcgetattr(0,&ttystate);     //读取当前的终端状态
        ttystate.c_lflag &= ~ICANON;    //关闭缓冲
        ttystate.c_cc[VMIN] = 1;		  //每次接收一个字符
        tcsetattr(0,TCSANOW,&ttystate); //  加载状态
    }
    

    接受输入(**==get_response(QUESTION);==**​​​)
    int get_response(char* )
    {
        int input;
        //输出提问
        printf("%s (y/n)?",QUESTION);
    
        //等待用户输入
        while (1)
        {
            switch (input = getchar())
            {
            case 'y'  :
            case 'Y'  : return 0;
            case 'n'  : 
            case 'N'  : 
            case EOF  : return  1;
            default   : 
                printf("\nCannot understand %c ",input);
                printf("Please type y or no \n");       
            }
        }
          
    }   
    

    问题

    如果这个程序运行在 ATM 上,而顾客在输入 y 或 n 之前走开了,将会怎样?下一个顾客跑来按 y,就能进入那个离开的顾客账号。所以用户程序包含超时特征,会变得更安全。


    超级版本:等待输入版本

    版本改进

    此版本具有超时特征。通过设置终端驱动程序,使之不等待输人来实现这个特征,先检查看是否有输入,如果发现没有输人,则先睡眠几秒钟,然后继续检查输人。如此尝试 3 次之后放弃。

    思路

    注意到,我们的程序可以检测用户不输入状态。如何做到?

    默认情况下终端是有阻塞模式的,程序会等待用户输入,然后才检测用户输入。

    如果我们处于阻塞模式,程序就会检测不到我们没有输入,所以我们必须把阻塞关掉。

    如果我们关闭阻塞模式,程序会直接判断我们的输入,如果我们没输入,则 read 程序会返回 0。

    主要思路(输入思路)

    • 关闭回显、缓冲和阻塞 - 解决判断用户不输入的问题

    • 睡眠!

    • 等待输入 1 - 无视错误输入

      • 如果检测不到 yYnN,就会一直在 while 循环里,做不到其他事情

      ==**while (strchr("yYnN",c) == NULL) ; //不论如何,程序是不会锁死在while循环里的**==

      问题:但是因为关闭了阻塞,所以如果我们不输入或者错误输入,strchr 会让 while 卡死在循环里

    • 等待输入 2 - 解决无法退出循环的问题

      **==while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL) ;==**​​

    #include <stdio.h>
    #include <stdlib.h>
    #include <termios.h>
    #include <fcntl.h>
    #include <string.h>
    #include <ctype.h>
    #include <unistd.h>

    /*
    万一有人在输入 y 之前走开了,程序会不安全。
    因此要加入超时判断
    */

    /*
    设计思路:
    设置终端驱动程序,使之不等待输入来实现这个特征。先检查是否由输入,然后沉睡,再检查输入
    如此往复 3 次后退出
    */
    #define ASK "Do you want another transaction?"
    #define TRIES 3 //尝试 3 次后退出
    #define SLEEPTIME 3 //沉睡事件间隔
    #define BEEP putchar('\a'); //警告用户

    int get_response(char* ,int);
    void set_cr_noecho_mode();
    void tty_mode(int);
    void set_nodelay_mode();
    int get_ok_char();

    int main()
    {
    int response;
    //保存状态
    tty_mode^12;

    //关闭回显  
    set_cr_noecho_mode()[^13];
    
    //关闭阻塞  
    set_nodelay_mode();[^14]
    

    ==​ //得到输入,重点!!!==
    response = get_response^15;

    //恢复出厂设置  
     tty_mode[^12](1);  
    return response;  
    

    }

    tty_mode(0/1);
    //读取状态,保存状态
    void tty_mode(int how)
    {
        static struct termios origin_mode;
        static int origin_flags;
        if(how == 0){
            tcgetattr(0,&origin_mode);
            origin_flags = fcntl(0,F_GETFL);
        }   
        else{
            tcsetattr(0,TCSANOW,&origin_mode); 
            fcntl(0,F_SETFL,origin_flags); 
        }
    }
    

    set_cr_noecho_mode()
    //关闭回显与缓冲
    void set_cr_noecho_mode()
    {
        static struct termios ttystate;
        tcgetattr(0,&ttystate);     //读取当前的终端状态   
        ttystate.c_lflag &= ~ICANON;    //关闭缓冲
        ttystate.c_lflag &= ~ECHO;      //关闭回显
        ttystate.c_cc[VMIN] = 1;        //每输入一个字符响应一次
        tcsetattr(0,TCSANOW,&ttystate);     //加载设置
    }
    

    set_nodelay_mode();
    //将文件描述符设置为非阻塞模式
    void set_nodelay_mode()
    {
        int termflags;
        termflags = fcntl(0,F_GETFL);   //获取文件当前的状态位
        termflags |= O_NDELAY;         //改变状态为非阻塞模式
        fcntl(0,F_SETFL,termflags);     //加载状态位
    }
    

    get_response ()
    • 主要函数:get_ok_char()1
    //得到响应
    int get_response(char* question , int maxtries )
    {
        int input;
        //输出提问
        printf("%s (y/n)?",question);
        fflush(stdout);     //清空缓冲区,强制输出
    
        //等待用户输入
        while (1)
        {
           printf("sleep now \n");
           sleep(SLEEPTIME);
           printf("sleep finish \n");
           input = tolower(get_ok_char());   //得到下一个字符,且将大写字母转为小写字母
           if(input == 'y')
                return 0;
           if(input == 'n')
                return 1;
           if(maxtries-- == 0){         //超时
                printf("超时退出\n");
                return 2;
           }  
            BEEP;
        }   
    }   
    

    • get_ok_char()

      
      //得到下一个字符
      int get_ok_char()
      {
          int c;
      //查找字符c,由于关闭了阻塞模式,故不会卡在while循环
      //程序会直接读取用户输入,在不输入的情况下,getchar与strchr得到的是EOF与NULL
          while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL)
              ;
          return c;
      }
      

  8. 保存出厂设置与恢复出厂设置(==**int tty_mode(int);**==​)

    思路

    设置一个静态变量用来保存原有的驱动状态

    //读取状态,保存状态
    int tty_mode(int how)
    {
        static struct termios origin_mode;
        if(how == 0)
            tcgetattr(0,&origin_mode);
        else
            return tcsetattr(0,TCSANOW,&origin_mode); 
    }
    

  9. 设置驱动模式,关闭缓存(**==set_mode();==**​​​​​)

    思路

    • 关闭缓存标志位
    void set_mode()
    {
        struct termios ttystate;
        tcgetattr(0,&ttystate);     //读取当前的终端状态
        ttystate.c_lflag &= ~ICANON;    //关闭缓冲
        ttystate.c_cc[VMIN] = 1;		  //每次接收一个字符
        tcsetattr(0,TCSANOW,&ttystate); //  加载状态
    }
    

  10. 接受输入(**==get_response(QUESTION);==**​​​)
    int get_response(char* )
    {
        int input;
        //输出提问
        printf("%s (y/n)?",QUESTION);
    
        //等待用户输入
        while (1)
        {
            switch (input = getchar())
            {
            case 'y'  :
            case 'Y'  : return 0;
            case 'n'  : 
            case 'N'  : 
            case EOF  : return  1;
            default   : 
                printf("\nCannot understand %c ",input);
                printf("Please type y or no \n");       
            }
        }
          
    }   
    
  11. tty_mode(0/1);
    //读取状态,保存状态
    void tty_mode(int how)
    {
        static struct termios origin_mode;
        static int origin_flags;
        if(how == 0){
            tcgetattr(0,&origin_mode);
            origin_flags = fcntl(0,F_GETFL);
        }   
        else{
            tcsetattr(0,TCSANOW,&origin_mode); 
            fcntl(0,F_SETFL,origin_flags); 
        }
    }
    

  12. set_cr_noecho_mode()
    //关闭回显与缓冲
    void set_cr_noecho_mode()
    {
        static struct termios ttystate;
        tcgetattr(0,&ttystate);     //读取当前的终端状态   
        ttystate.c_lflag &= ~ICANON;    //关闭缓冲
        ttystate.c_lflag &= ~ECHO;      //关闭回显
        ttystate.c_cc[VMIN] = 1;        //每输入一个字符响应一次
        tcsetattr(0,TCSANOW,&ttystate);     //加载设置
    }
    

  13. set_nodelay_mode();
    //将文件描述符设置为非阻塞模式
    void set_nodelay_mode()
    {
        int termflags;
        termflags = fcntl(0,F_GETFL);   //获取文件当前的状态位
        termflags |= O_NDELAY;         //改变状态为非阻塞模式
        fcntl(0,F_SETFL,termflags);     //加载状态位
    }
    

  14. get_response ()
    • 主要函数:get_ok_char()1
    //得到响应
    int get_response(char* question , int maxtries )
    {
        int input;
        //输出提问
        printf("%s (y/n)?",question);
        fflush(stdout);     //清空缓冲区,强制输出
    
        //等待用户输入
        while (1)
        {
           printf("sleep now \n");
           sleep(SLEEPTIME);
           printf("sleep finish \n");
           input = tolower(get_ok_char());   //得到下一个字符,且将大写字母转为小写字母
           if(input == 'y')
                return 0;
           if(input == 'n')
                return 1;
           if(maxtries-- == 0){         //超时
                printf("超时退出\n");
                return 2;
           }  
            BEEP;
        }   
    }   
    

    • get_ok_char()

      
      //得到下一个字符
      int get_ok_char()
      {
          int c;
      //查找字符c,由于关闭了阻塞模式,故不会卡在while循环
      //程序会直接读取用户输入,在不输入的情况下,getchar与strchr得到的是EOF与NULL
          while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL)
              ;
          return c;
      }
      

    • get_ok_char()
    
    //得到下一个字符
    int get_ok_char()
    {
        int c;
    //查找字符c,由于关闭了阻塞模式,故不会卡在while循环
    //程序会直接读取用户输入,在不输入的情况下,getchar与strchr得到的是EOF与NULL
        while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL)
            ;
        return c;
    }
    

  15. 8-进程和程序:编写命令解释器 sh

    ​学习系统调用1​​

    • execvp1

    • fork1

    • wait/exit1

    什么是进程?

    进程就是正在运行的程序。

    shell 是管理进程和运行程序的进程,其主要功能有三:

    1. 运行程序
    2. 管理输入和输出
    3. 可编程

    例子:

    ​![image](https://b3logfile.com/file/2023/08/siyuan/1690891567944/assets/image-20230820195005-c5c70im.png)​
    

    shell 运行原理

    一开始我们处在 shell 进程中,我们想要运行其他程序,就需要开辟一个新的进程。比如:shell 从用户读入字符串“ls"。shell 建立一个新的进程,然后在那个新的进程中运行 ls 程序并等待那个进程结束。

    image

    所以,为了写一个 shell,我们需要学会:

    1. 运行一个程序
    2. 建立一个进程
    3. 等待 exit().

    编写自己的 shell

    shell 最基础的功能之一就是在 shell 中可以调用其他进程。

    问题 1 :如何在一个程序中运行另一个进程?

    **答案:**调用了 execvp1

    程序编写思路

    • 建立一个字符串数组,存储用户输入(用 fgets 函数)

    • fgets 函数会保存用户输入的回车符号,因此需要专门的函数将回车符替换成 '\0'

    • 调用 execvp


    #include <stdio.h>
    #include <signal.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    
    #define MAXARGS 20		//输入指令的最大数量
    #define ARGLEN  100		//一个指令最大的字符数
    
    int execute(char *arglist[]);
    
    int main()
    {
        char *arglist[MAXARGS + 1];		//用于存储指令的数组
        int numargs;
        char argbuf[ARGLEN];
        char *makestring();				//用于将fgets最后的回车替换成'\0'
        numargs = 0;
        while(numargs < MAXARGS)
        {
            printf("Arg[%d]?",numargs);
            if(fgets(argbuf,ARGLEN,stdin) && *argbuf != '\n')
                arglist[numargs++] = makestring(argbuf);
            else
            {
                if(numargs > 0){
                    arglist[numargs] = NULL;
                    execute(arglist);
                    numargs = 0;
                }
            }
        }
        return 0;
    }
    
    ----------------------------------------------------------------
    
    //调用execvp替换掉原来的程序
    
    ----------------------------------------------------------------
    int execute(char *arglist[])
    {
        execvp(arglist[0],arglist);
        perror("execvp failed");
        exit(1);
    }
    
    -----------------------------------------------------------------
    /*
    	把fgets中的回车去掉
    */
    -----------------------------------------------------------------
    char *makestring(char * buf){
        char *cp;
        buf[strlen(buf) - 1] = '\0';        ///////////把回车替换成'\0'
        cp = malloc(strlen(buf) + 1);
        if(cp == NULL){
            fprintf(stderr,"no memory\n");
            exit(1);
        }
        strcpy(cp,buf);
        return cp;
    }
    

    问题 2 :调用完其他程序后,shell 程序如何不退出?

    但是上述代码有些缺陷,execvp用命令指定的程序覆盖了shell的程序代码,然后在命令指定的程序结束后退出。这样shell就不能再次接受新的命令了,为了运行新的命令,用户不得不再次运行shell。
    
    shell如何能做到在运行程序的同时还能等待下一个命令呢?方法之一就是启动一个新的进程,由这个程序执行命令程序,再返回shell。
    
    	
    

    编程思路

    shell调用fork[^19]系统调用,建立新的进程
    
    在新的进程里调用execvp,调用用户指定的命令
    
    父进程调用wait/exit[^20]等待子进程返回。
    

    /*
        实现一个真正的shell
    */
    
    #include <stdio.h>
    #include <signal.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include<sys/wait.h>
    #include <sys/types.h>
    #include <signal.h>
    
    
    #define MAXARGS 20
    #define ARGLEN  100
    
    void execute(char *arglist[]);
    
    int main()
    {
        char *arglist[MAXARGS + 1];
        int numargs;
        char argbuf[ARGLEN];
        char *makestring();
    
        numargs = 0;
        while(numargs < MAXARGS)
        {
            printf("Arg[%d]?: ",numargs);
            if(fgets(argbuf,ARGLEN,stdin)  && *argbuf != '\n')
                arglist[numargs++] = makestring(argbuf);  //把最后的回车去掉
            else
            {
                if(numargs > 0){
                    arglist[numargs] = NULL;
                    execute(arglist);
                    numargs = 0;
                }
            }
        }
        return 0;
    }
    
    //将fgets函数中的回车替换成'\0'
    char* makestring(char * buf)
    {
        char *cp;
    
        buf[strlen(buf) - 1] = '\0';
    
        cp = malloc(strlen(buf) + 1);
    
        if(cp == NULL){
            fprintf(stderr,"no memory\n");
            exit(1);
        }
        strcpy(cp,buf);
        return cp;
    }
    
    //执行用户指定的命令
    void execute(char *arglist[])
    {
        int pid,exitstatus;
    
        pid = fork();
    
        switch (pid)
        {
        case -1:
            perror("fork failed");
            break;
        //在子进程中执行程序
        case 0:
            execvp(arglist[0],arglist);
            perror("execvp failed");
            sleep(7);
            exit(1);
        //父进程等待子进程返回
        default:
            signal(SIGINT,SIG_IGN);
            while( wait(&exitstatus) != pid) ;
    
            printf( "child exited with status %d, %d\n",
                    exitstatus>>8,exitstatus & 0377);
            break;
        }
    }
    
    

    • 作用:在一个程序中调用另一个程序

    • 函数原型:int execvp(const char __file, ​char​ ​const****​​ ​​*****__argv)

    • 注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。

    • 详解:

      image

    • 实例:调用 ls

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      
      
      int main()
      {
          char *arglist[3];
      
          arglist[0] = "ls";
          arglist[1] = "-l";
          arglist[2] = 0;
      
          printf("*** About to exec ls -l\n");
          execvp("ls",arglist);
          printf("*** ls is done. bye\n");
      }
      

  16. fork

    • fork 的意义

      我们在用 execvp 调用新的进程后,会把原来的进程给替换掉。比如说在我们自己编写的 shell 进程中调用 ls 后,shell 进程也被关闭了。

      而我们希望 execvp 调用后,还能返回到原来的 shell 进程中。

      所以我们可以通过 fork 建立一个新的进程,然后在新进程中调用 execvp,父进程调用 wait 等待新进程执行完毕,当新进程执行 exit 时,父进程就会收到信号,然后父进程继续运行

    • fork 详解

      image

    fork 的简单应用

    /*
        建立新的进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    
    int main()
    {
        int ret_from_fork,mypid;
    
        mypid = getpid();
    
        printf("Before: my pid is %d\n",mypid);
    
        ret_from_fork = fork();
    
        sleep(1);
        printf("After: my pid is %d, fork() said %d\n",
            getpid(), ret_from_fork);
    }
    

    输出:

    image

    fork 的特征(区分父进程和子进程)

    • fork 会建立一个新的进程
    • fork()函数会返回进程号,在子进程中,返回的进程号是 0;父进程中,返回的进程号是子进程的进程号

    • 实例:
    /*
        判断自己是子进程还是父进程
    */
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main()
    {
        int fork_rv;
    
        printf("Before : my pid is %d\n",getpid());
    
        fork_rv = fork();       //在子进程中,fork()返回0
    
        if(fork_rv == -1)
            perror("fork");
          
        else if(fork_rv == 0)
            printf("I am the child. my pid = %d\n",getpid());
        else
            printf("I am the parent. my child is %d\n",fork_rv);
    }
    

    输出:

    image

  17. wait/exit

    • 作用:进程调用 wait 等待子进程结束

    • 用法:pid = wait(&status)

    • wait 详解

      image

    • 系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。

    • statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。

    image

    • 实例: waitdemo.c

      该例子显示了子进程调用 exit 是如何触发 wait 返回父进程并如何得到子进程返回时的状态 的。

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include<sys/wait.h>
    
    #define DELAY   5
    
    int main()
    {
        int newpid;
        void child_code(), parent_code();
    
        printf("before: mypid is %d\n",getpid());
    
        if( (newpid = fork()) == -1)
            perror("fork");
        else if( newpid == 0)	
            child_code(DELAY);	//执行子进程的代码
        else
            parent_code(newpid);		//父进程代码
    }
    
    //子进程执行的代码
    void child_code(int delay)
    {
        printf("child %d here . will sleep for %d seconds\n",getpid(),delay);
        sleep(delay);
        printf("child done about to exit\n");
        exit(17);
    }
    
    //父进程代码,等待子进程结束
    void parent_code(int childpid)
    {
        int wait_rv;        //return value from wait()
        int child_status;
        int high_8 , low_7, bit_7;
         
    //等待子进程结束
        wait_rv = wait(&child_status);
    
    //子进程结束后,执行以下代码
        printf("child_status: %p\n",*(&child_status) >> 8);
        printf("done waiting fot %d.&Wait returned: %d\n",childpid,wait_rv);
    
    //得到子进程退出时的状态
        high_8 = child_status >> 8;     //1111 1111 0000 0000
        low_7 = child_status &0x7F;     //0000 0000 0111 1111
        bit_7 = child_status & 0x80;    //0000 0000 1000 0000
        printf("status: exit = %d, sig = %d, core = %d\n",high_8, low_7 ,bit_7);
    }
    
    

    image

  18. 9-可编程的 shell、shell 变量和环境:编写自己的 shell

  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    939 引用 • 940 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • CloudFoundry

    Cloud Foundry 是 VMware 推出的业界第一个开源 PaaS 云平台,它支持多种框架、语言、运行时环境、云平台及应用服务,使开发人员能够在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构的问题。

    5 引用 • 18 回帖 • 164 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 726 关注
  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    247 引用 • 1347 回帖
  • 工具

    子曰:“工欲善其事,必先利其器。”

    285 引用 • 728 回帖
  • 音乐

    你听到信仰的声音了么?

    60 引用 • 511 回帖
  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    14 引用 • 106 回帖 • 1 关注
  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 461 关注
  • Swagger

    Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。

    26 引用 • 35 回帖 • 3 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 47 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    36 引用 • 37 回帖 • 534 关注
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 624 关注
  • 阿里云

    阿里云是阿里巴巴集团旗下公司,是全球领先的云计算及人工智能科技公司。提供云服务器、云数据库、云安全等云计算服务,以及大数据、人工智能服务、精准定制基于场景的行业解决方案。

    89 引用 • 345 回帖
  • Markdown

    Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。

    167 引用 • 1509 回帖
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖 • 18 关注
  • Git

    Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    209 引用 • 358 回帖
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    17 引用 • 236 回帖 • 333 关注
  • 30Seconds

    📙 前端知识精选集,包含 HTML、CSS、JavaScript、React、Node、安全等方面,每天仅需 30 秒。

    • 精选常见面试题,帮助您准备下一次面试
    • 精选常见交互,帮助您拥有简洁酷炫的站点
    • 精选有用的 React 片段,帮助你获取最佳实践
    • 精选常见代码集,帮助您提高打码效率
    • 整理前端界的最新资讯,邀您一同探索新世界
    488 引用 • 383 回帖 • 7 关注
  • App

    App(应用程序,Application 的缩写)一般指手机软件。

    91 引用 • 384 回帖 • 1 关注
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    490 引用 • 916 回帖 • 1 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖 • 1 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 97 关注
  • Hprose

    Hprose 是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。你无需专门学习,只需看上几眼,就能用它轻松构建分布式应用系统。

    9 引用 • 17 回帖 • 615 关注
  • Bug

    Bug 本意是指臭虫、缺陷、损坏、犯贫、窃听器、小虫等。现在人们把在程序中一些缺陷或问题统称为 bug(漏洞)。

    75 引用 • 1737 回帖 • 1 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖
  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    52 引用 • 228 回帖 • 1 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 1 关注