UNIX/Linux学习记录

本文主要记录了阅读《UNIX/Linux编程实践教程》过程中的学习笔记

环境配置相关问题

centos7更换镜像源:

  1. 安装wget
    yum install -y wget

  2. 备份服务器原有的yum源文件
    mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

  3. 下载阿里云镜像文件
    wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

  4. 清理缓存
    yum clean all

  5. 生成缓存
    yum makecache

  6. 更新最新源设置
    yum update -y

vim插件的使用

参考文档

安装管理vim插件的插件pathogen

1
2
$ mkdir -p ~/.vim/autoload ~/.vim/bundle && \
curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim

创建该插件所需要的目录, 把 pathogen.vim 安装在 ~/.vim/autoload 目录,vim 会自动载入这个目录下的 pathogen 插件(目录名 autoload 顾名思义),将插件交给 pathogen 管理。创建 ~/.vim/bundle 目录是用来存放管理以后要安装插件。

在 .vimrc 里配置,加上下面这句话,意思是执行这个函数 infect()

1
2
3
4
5
6
7
execute pathogen#infect()

# 如果没有 .vimrc 文件, 则 vim ~/.vimrc ,然后黏贴下面这几句话

execute pathogen#infect()
syntax on
filetype plugin indent on

以后直接把插件安装到~/.vim/bundle目录下即可

auto-pairs自动补齐括号、引号插件

~/.vim/bundle目录下

1
2
$ git clone git@github.com:jiangmiao/auto-pairs.git

NERDTree树状文件列表

安装:

1
$ git clone git@github.com:preservim/nerdtree.git
  • 打开NERDTree: shift + : 然后输入 NERDTree 可看到结果
  • ctrl + w + h 光标切换到左侧树形目录
  • ctrl + w + l 光标切换到右侧文件显示窗口

更多使用方式

安装 MiniBufExplorer 的改进版

1
$ git clone git@github.com:fholgado/minibufexpl.vim.git ~/.vim/bundle/minibufexpl

测试, 可以打开多个 buffer, :vsp 加文件名

taglist

taglist 依赖 ctags ,默认自带, 没有就自己安装

1
sudo yum install ctags

下载 taglist 到 bundle 目录

1
git clone git@github.com:vim-scripts/taglist.vim.git

测试, 用 vim 打开一个文件,然后输入 TlistToggle

注意修改完文件后要保存重新进以后才能更新tag

C++自动补齐插件OmniCppComplete

1
git clone git@github.com:vim-scripts/OmniCppComplete.git

在 test 目录,生成 tags 文件,这样才能自动补全:

1
2
ctags -R --sort=yes --c++-kinds=+p --fields=+iaS --extra=+q --language-force=C++
#ctags -R --c++-kinds=+p --fields=+iaS --extra=+q ./

自动补全快捷键 ctr+x ctr+o , 上下选择快捷键 ctr+n ctr+p。

全部配置完成后的.vimrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
execute pathogen#infect()
syntax on
filetype plugin indent on

""""""""""""""""""""""
" Tag List (ctags)
""""""""""""""""""""""
"set mouse=a " always use mouse
let Tlist_Ctags_Cmd = '/usr/bin/ctags'
let Tlist_Show_One_File = 1 "不同时显示多个文件的tag,只显示当前文件的
let Tlist_Exit_OnlyWindow = 1 "如果taglist窗口是最后一个窗口,则退出vim
let Tlist_Use_Right_Wiset = 1
let Tlist_Sort_Type="name" " tag按名字排序

set tags+=~/.vim/tags/cpp_src/tags

"""""""""""""""""""""
" omnicppcomplete
"""""""""""""""""""""
filetype plugin indent on
set completeopt=longest,menu
let OmniCpp_NamespaceSearch = 2 " search namespaces in the current buffer and in included files
let OmniCpp_ShowPrototypeInAbbr = 1 " 显示函数参数列表
let OmniCpp_MayCompleteScope = 1 " 输入 :: 后自动补全
let OmniCpp_DefaultNamespaces = ["std", "_GLIBCXX_STD"]

Linux常用命令

注意所有命令几乎都是人为编写的程序(大多都是C语言写的)

在Unix系统下增加新的命令只需要把程序的可执行文件放到以下任意目录即可:

  • /bin
  • /usr/bin
  • /usr/local/bin

目录树样例:

程序运行和注销

  • exit用于注销已运行的程序

目录操作

  • ls:列出目录内容
    • ls /:列出根目录内容
  • cd:改变当前目录
    • cd ..:回退到上一层目录
  • pwd:显示当前目录路径
  • mkdir rmdir:新建、删除目录

文件操作

注意文件名不能包含根目录符号 /

  • cat more less pg 查看文件内容
  • cp:文件赋值
  • rm:文件删除(注意Unix不提供恢复被删除文件的功能,其中一个原因是由于Unix是一个多用户系统,文件被删掉以后存储空间可能立即被分配给其他用户的文件)
  • mv:重命名或移动文件
  • lpr:打印文件
  • ls -l:查看文件详细信息,可以看到文件权限
    • 共三组rwx,表示user group other是否可以read write excute

bc: Unix计算器

输入bc即可启动计算器,使用Ctrl + D退出

bc还是可编程的,语法与C语言类似

其他

  • ps:列出系统中运行的所有进程

  • man:查看帮助

    • 使用 man -k可以根据关键字搜索联机帮助
  • who:列出当前系统中活动的用户

设计自己的who命令

1
2
3
4
5
6
$ man who
#根据描述可以发现who是用于查询所有登录用户的信息的,放在文件/var/run/utmp中

$ man -k utmp
$ man 5 utmp
#可以找到utmp的数据结构

问题:如何从文件中读取数据结构

可以在帮助中寻找答案,寻找与file和read相关的帮助。

由于man命令选项-k(根据关键字查找)只支持一个关键字的查找,可以借助grep命令查找(使用正则表达式搜索文本,并打印匹配的行)

1
2
$ man -k file | grep read
$ man 2 read

查看read系统调用的说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
NAME
read - read from a file descriptor

SYNOPSIS
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

DESCRIPTION
read() attempts to read up to count bytes from file descriptor fd into
the buffer starting at buf.

On files that support seeking, the read operation commences at the cur‐
rent file offset, and the file offset is incremented by the number of
bytes read. If the current file offset is at or past the end of file,
no bytes are read, and read() returns zero.

If count is zero, read() may detect the errors described below. In the
absence of any errors, or if read() does not check for errors, a read()
with a count of 0 returns zero and has no other effects.

If count is greater than SSIZE_MAX, the result is unspecified.

......

SEE ALSO
close(2), fcntl(2), ioctl(2), lseek(2), open(2), pread(2), readdir(2),
readlink(2), readv(2), select(2), write(2), fread(3)
  • 这个系统调用可以将一定数目的字节读入一个缓冲区。

  • 因为每次都要读入一个数据结构,所以要用sizeof(struct utmp)来指定每次读入的字节数

  • read()需要一个文件描述符作为输入参数,文件描述符通过open()获取

答案:使用open、read和close

  1. 打开一个文件:int open(const char *pathname, int flags);
    • 这个系统调用在进程和文件之间建立一条连接,这个连接被称为文件描述符,它就像一条由进程通向内核的管道。
    • 如果文件被顺利打开,内核就会返回一个正整数的值,即文件描述符,用来唯一标识这个连接。(打开不同的文件或者同一文件打开多次得到的文件描述符是不同的)
    • The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the file read-only, write-only, or read/write, respectively.
  2. 从文件读取数据:ssize_t read(int fd, void *buf, size_t count);
    • 这个系统调用请求内核从fd指定的文件中读取count字节的数据,存放到buf所指定的内存空间中,如果成功读取了数据,就返回读取的字节数目,否则返回-1.
  3. 关闭文件:int close(int fd);
    • 当不需要对文件进行读写操作时,就把文件关闭
    • 这个系统调用会关闭进程和文件fd之间的连接,如果关闭的过程中出现错误,close返回-1,如:fd所指的文件不存在等

编写who1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# include<stdio.h>
# include<utmp.h>
# include<fcntl.h>
# include<unistd.h>

#define SHOWHOST

int main(){
struct utmp current_record;
int utmpfd; //存储utmp文件的文件描述符
int reclen = sizeof(current_record);

//UTMP_FILE定义在/usr/include/utmp.h中, O_RDONLY表示只读模式打开文件
if((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1){
perror(UTMP_FILE);
exit(1);
}
//一次读取一个数据结构,重复到读完为之
while(read(utmpfd, &current_record,reclen) == reclen){
show_info(&current_record);
}
close(utmpfd);
return 0;
}

//显示登录信息
show_info(struct utmp *utbufp){
printf("% - 8.8s",utbufp->ut_name);
printf(" ");
printf("% - 8.8s",utbufp->ut_line);
printf(" ");
printf("%10ld",utbufp->ut_time);//以long int格式输出
printf(" ");
#ifdef SHOWHOST
printf("%s",utbufp->ut_line);
#endif
printf("\n");
}

将上述代码编译、运行

1
2
$ cc who1.c -o who1
$ ./who1

与系统的who命令输出对比

1
2
3
4
5
6
7
8
9
[zhangqi@localhost test]$ who
zhangqi :0 2022-08-30 06:58 (:0)
zhangqi pts/0 2022-08-31 01:59 (:0)

[zhangqi@localhost test]$ ./who1
reboot ~ 1661813849 ~
runlevel ~ 1661813884 ~
zhangqi :0 1661813911 :0
zhangqi pts/0 1661882367 pts/0

存在问题:

  1. 需要消除空白记录
  2. 登陆时间没有正确显示

编写who2.c

  • 如何消除空白记录:

    • 空白记录产生的原因是utmp包含所有终端的信息,包括那些尚未被用到的终端信息。

    • 因此要做到能够区分出哪些终端对应活动的用户:

      • 在utmp.h中定义了如下内容:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        /* Values for ut_type field, below */

        #define EMPTY 0 /* Record does not contain valid info (formerly known as UT_UNKNOWN on Linux) */
        #define RUN_LVL 1 /* Change in system run-level (see init(8)) */
        #define BOOT_TIME 2 /* Time of system boot (in ut_tv) */
        #define NEW_TIME 3 /* Time after system clock change (in ut_tv) */
        #define OLD_TIME 4 /* Time before system clock change (in ut_tv) */
        #define INIT_PROCESS 5 /* Process spawned by init(8) */
        #define LOGIN_PROCESS 6 /* Session leader process for user login */
        #define USER_PROCESS 7 /* Normal process */
        #define DEAD_PROCESS 8 /* Terminated process */
        #define ACCOUNTING 9 /* Not implemented */

        可知utmp结构中的ut_type成员,当其值为7(USER_PROCESS)时,表示这是一个已经登录的用户

      • 因此对原程序进行修改(我的方式是在while循环中增加判断,不走show_info,书中是在show_info中进行判断的)

        1
        2
        3
        4
        while(read(utmpfd, &current_record,reclen) == reclen){
        if((&current_record)->ut_type != USER_PROCESS) continue;
        show_info(&current_record);
        }
  • 以可读的方式显示登录时间

    • 可在联机帮助中搜索关于时间的主题:

      1
      2
      3
      $ man -k time
      $ man -k time | grep transform
      $ man -k time | grep -i convert

      可以发现很多记录都涉及到time.h

    • 了解可的值Unix储存时间的方式是 time_t数据类型 time.h中有typedef lont int time_t,表示从1970年1月1日0时开始所经过的秒数

    • 将time_t显示出来用到ctime,将整数值转换为常用的时间形式:

      char *ctime(const time_t *timep);

    • 注意不是所有的字符串内容都需要(去掉周几和年份,只返回ctime返回字符串从第4个字符串开始的12个字符)

      printf("%12.12s",ctime(&t) + 4);

整体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<stdio.h>
#include<utmp.h>
#include<fcntl.h>
#include<unistd.h>
#include<time.h>

#define SHOWHOST

void display_time(long);
void show_info(struct utmp*);

int main(){
struct utmp current_record;
int utmpfd;
int reclen = sizeof(current_record);

if((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1){
perror(UTMP_FILE);
exit(1);
}
while(read(utmpfd, &current_record,reclen) == reclen){
if((&current_record)->ut_type != USER_PROCESS) continue;
show_info(&current_record);
}
close(utmpfd);
return 0;
}

void show_info(struct utmp *utbufp){
printf("% - 8.8s",utbufp->ut_name);
printf(" ");
printf("% - 8.8s",utbufp->ut_line);
printf(" ");
display_time(utbufp->ut_time);
printf(" ");
#ifdef SHOWHOST
printf("%s",utbufp->ut_line);
#endif
printf("\n");
}

void display_time(long timeval){
char* cp;
cp = ctime(&timeval);
printf("%12.12s",cp + 4);
}

编写cp(读和写)

cp命令的典型用法:

cp source-file target-file

如果target-file所指定的文件不存在,cp就创建这个文件,如果已经存在就覆盖,target-file的内容与source-file相同

cp命令是如何创建/重写文件的

  • 其中一种方法是使用系统调用函数creat:

    int creat(const char *pathname, mode_t mode);

    • creat告诉内核创建一个名为pathname的文件,如果这个文件不存在,就创建它,如果已经存在,就把它的内容清空,把文件长度设为0

    • 如果内核成功创建了文件,那么文件的许可位(permission bits)被设置为由第2个参数mode所指定的值,如:

      fd = creat("addressbook", 0644);

      创建一个名为addressbook的文件,如果文件不存在,那么文件的许可位被设为rw-r-r–

      返回值是指向addressbook的文件描述符

  • 用write系统调用向已打开的文件中写入数据:

    ssize_t write(int fd, const void *buf, size_t count);

    • fd文件描述符,buf内存数据,count要写的字节数
    • 写入失败返回-1,写入成功返回写入的字节数
    • 注意实际写入的字节数可能少于count(有的系统对文件最大尺寸有限制或磁盘空间接近满了)
    • 因此调用write后必须检查返回值是否与要写入的相同,若不同作出相应的处理

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFFERSIZE 4096
#define COPYMODE 0644

void oops(char *, char *);

main(int ac, char *av[]){
int in_fd, out_fd, n_chars;
char buf[BUFFERSIZE];

if(ac!=3){
fprintf(stderr, "usage: %s source destination\n", *av);
exit(1);
}

if((in_fd = open(av[1],O_RDONLY)) == -1)//判断能否打开
oops("Cannot open ", av[1]);
if((out_fd = creat(av[2],COPYMODE)) == -1)//判断能否创建
oops("Cannot creat ", av[2]);

while((n_chars = read(in_fd, buf, BUFFERSIZE)) > 0)
if(write(out_fd,buf,n_chars) != n_chars)
oops("Write error to ",av[2]);
if(n_chars == -1)
oops("Read error from ",av[1]);

if(close(in_fd) == -1 || close(out_fd) == -1)
oops("Error closing files","");

}

void oops(char *s1, char *s2){
fprintf(stderr,"Error: %s",s1);
perror(s2);
exit(1);
}

编译并测试:

1
2
3
$ cc cp1.c -o cp1
$ ./cp1 cp1 copy.of.cp1
$ cmp cp1 copy.of.cp1 #Unix自带的文件比较工具cmp,无任何提示则说明内容完全相同

提高文件I/O效率的方法:使用缓冲

注意cp1中定义了BUFFERSIZE这个常量,用于标识每次读/写操作的数据长度,这里的值是4096。

要注意缓冲区大小对性能有很大的影响,使用大的缓冲区可以减少系统调用的次数(在cp1中就是减少read()和write()的次数)

为什么系统调用耗时很长:

  • 用户进程位于用户空间,内核位于系统空间,磁盘只能被内核直接访问
  • 当系统调用发生时,执行权会从用户代码转移到内核代码,执行内核代码是需要时间的
  • 由于运行内核代码时,CPU工作在管理员(supervisor)模式,这对应于一些特殊的对战和内存环境,必须在系统调用发生时建立好。
  • 系统调用结束后,CPU要切换到用户模式,必须把堆栈和内存环境恢复成用户程序运行时的状态,这种运行环境的切换要消耗很多时间。(上下文切换)

在who2.c中运用缓冲技术

使用一个能容纳16个utmp结构的数组作为缓冲区,标识为buffer。

我们需要编写utmp_next函数来从缓冲区中取得下一个utmp结构的数据

在utmplib.c中进行实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <utmp.h>

#define NRECS 16
#define NULLUT ((struct utmp *)NULL)
#define UTSIZE (sizeof(struct utmp))

static char utmpbuf[NRECS * UTSIZE];
static int num_recs;
static int cur_rec;
static int fd_utmp = -1;

int utmp_open(char* filename){
fd_utmp = open(filename, O_RDONLY);
cur_rec = num_recs = 0;
return fd_utmp;
}
struct utmp* utmp_next(){
struct utmp *recp;
if(fd_utmp == -1) return NULLUT;
//如果缓冲记录读到头并且文件记录也读完了,返回空指针
if(cur_rec == num_recs && utmp_reload()==0) return NULLUT;
recp = (struct utmp*)&utmpbuf[cur_rec * UTSIZE];
++cur_rec;
return recp;
}
int utmp_reload(){
int amt_read;
amt_read = read(fd_utmp, utmpbuf, NRECS*UTSIZE);
num_recs = amt_read/UTSIZE;
cur_rec = 0;
return num_recs;
}

void utmp_close(){
if(fd_utmp!=-1) close(fd_utmp);
}

主函数流程修改如下:

修改后的主函数没有直接对open、read和close进行调用,而是调用与之等价的具有缓冲模式的函数接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "utmplib.c"  //注意包含.c文件

int main(){
struct utmp *utbufp, *utmp_next();

if(utmp_open(UTMP_FILE) == -1){
perror(UTMP_FILE);
exit(1);
}
while((utbufp = utmp_next())!= ((struct utmp*)NULL)){
show_info(utbufp);
}
utmp_close();
return 0;
}

内核缓冲技术

主要思想是一次读入大量数据放入缓冲区,需要的时候从缓冲区取得数据。

用户态和内核态之间的切换需要消耗时间,相比之下,磁盘的I/O操作消耗的时间更多,为了提高效率,内核也是用缓冲技术来提高对磁盘的访问速度。

  • 正如utmp文件是用户登录记录的集合,磁盘是数据块的集合,内核会对磁盘上的数据块作缓冲,就像who程序缓冲utmp记录一样。
  • 内核将磁盘上的数据块复制到内核缓冲区中,当一个用户空间中的进程要从磁盘上读数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程的缓冲区中。
  • 当进程所要求的数据块不在内核缓冲区中时,内核会把相应的数据块加入到请求数据列表中,然后将该进程挂起,接着为其他进程服务
  • 等把相应的数据块从磁盘读到内核缓冲区后,再把数据复制到进程的缓冲区中,最后环形被挂起的进程。
  • 注意read就是把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区,它们不等价于数据在内核缓冲和磁盘之间的交换。内核在积累一定数量的写数据后再一次写入磁盘中。

该应用的结果:

  • 提高磁盘I/O效率
  • 优化磁盘写操作
  • 需要及时将缓冲数据写入磁盘(这是个缺点,比如当发生意外状况如断电时,内核来不及写入数据到磁盘,则更新的数据会丢失)

标准C函数的实现

标准C函数如fopen getc fclose fgets的实现都包含内核级缓冲,它们用到了一个结构FILE,并以此为基础构造了类似utmplib的中间层

FILE的结构定义在/usr/include/libio.h

文件读写

注销过程是如何工作的:(utmp记录的改变)

  • 打开文件utmp
    • fd = open(UTMP_FILE, O_RDWR);
  • 从utmp中找到包含你所在终端的登录记录
    • 在while循环中读取一条utmp记录,将它的ut_line字段跟终端名字做比较,若相等则调用修改函数
  • 对当前记录作修改
    • 负责注销的程序修改当前记录,再把它写回到文件utmp中
    • 即要把ut_type的值从USER_PROCESS改成DEAD_PROCESS,把ut_time字段的值改为注销时间,也就是当前时间
    • 把修改过的记录写会文件不能用write(write只会更新下一条记录而不会修改)
    • 使用系统调用lseek进行修改 off_t lseek(int fd, off_t offset, int whence);
  • 关闭文件
    • close(fd)

改变文件的当前位置

我们知道Unix每次打开一个文件都会保存一个指针来记录文件的当前位置

当从文件读数据时,内核从指针标明的地方开始,读取指定的字节,然后移动位置指针,指向下一个未被读取的字节,写文件的操作也是类似的。

指针是与文件描述符相关联的,而不是与文件关联,所以如果两个程序同时打开一个文件,这时会有两个指针,两个程序对文件的读操作不会相互干扰。

系统调用lseek的用法:

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • fd:文件描述符
  • offset:从基准位置开始的偏移量(可以是负值)
  • whence:
    • SEEK_SET:文件的开始
    • SEEK_CUR:当前位置
    • SEEK_END:文件结尾
  • 遇到错误返回-1,否则返回指针变化前的位置

如:

  • lseek(fd, -sizeof(struct utmp),SEEK_CUR); 把指针往前移一个utmp结构
  • lseek(fd, 10*sizeof(struct utmp), SEEK_SET);把指针移到第11个记录的开始位置
  • lseek(fd, 0, SEEK_END);write(fd, "hello", strlen("hello"));使指针移到文件末尾,然后写一个字符串到文件中

编写终端注销的代码

可以编写一个函数对注销的用户修改utmp中相应的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int logout_tty(char * line){
int fd;
struct utmp rec;
int len = sizeof(struct utmp);
int retval = -1;

if((fd = open(UTMP_FILE,O_RDWR)) == -1)
return -1;
//寻找并修改
while(read(fd,&rec,len) == len){
if(strncmp(rec.ut_line, line)){
rec.ut_type = DEAD_PROCESS;
if(time(&rec.ut_time != -1)){
if(lseek(fd, -len, SEEK_CUR) != -1){
if(write(fd,&rec,len) == len)
retval = 0;
}
}
break;
}
}
//关闭文件
if(close(fd) == -1){
retval = -1;
}
return retval;
}

处理系统调用中的错误

很多系统调用遇到错误会返回-1,表示出了某些问题,因此调用者每次都必须检查返回值,一旦检测到错误,必须做出相应的处理。

确定错误的种类errno

内核通过全局变量errno来指明错误的类型,每个程序都可以访问到这个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NAME
errno - number of last error

SYNOPSIS
#include <errno.h>


A common mistake is to do
if (somecall() == -1) {
printf("somecall() failed\n");
if (errno == ...) { ... }
}

where errno no longer needs to have the value it had upon return from
somecall() (i.e., it may have been changed by the printf(3)). If the
value of errno should be preserved across a library call, it must be
saved:

if (somecall() == -1) {
int errsv = errno;
printf("somecall() failed\n");
if (errsv == ...) { ... }
}

显示错误信息:perror(3)

1
2
3
4
5
6
7
8
9
#include<stdio.h>

void perror(const char *s);

#include <errno.h>

const char *sys_errlist[];
int sys_nerr;
int errno;

perror(string)这个函数会自己查找错误代码,在标准错误输出中显示出相应的错误信息,参数string是要同时显示出的描述信息

目录与文件属性:编写ls

  • ls默认动作是找出当前目录中所有文件的文件名,字典序排序后输出
  • ls -l(ls的长格式)会列出每个文件的详细信息,每一行代表一个文件和它的多个属性
  • ls *.c显示与*.c匹配的文件
  • ls -a列出的内容包含以”.”开头的文件
  • ls -lu 显示最后访问时间
  • ls -s 显示以块为单位的文件大小
  • ls -t输出时按时间排序
  • ls -F显示文件类型

Unix是如何组织磁盘上的文件的

磁盘上的文件和目录被组成一棵目录树,每个节点都是目录或文件

图中的大方框表示目录,大方框内的小方框表示文件,目录之间的连线表示目录之间的组织关系

Unix系统中,每个文件都位于某个目录中,在逻辑上是没有驱动器或卷的,当然在物理上一个系统可以有多个驱动器或分区,每个驱动器上都可以有分区,位于不同驱动器和分区上的目录通过文件树无缝地连接在一起。

软盘、光盘这些移动存储介质也被挂到文件树的某一个子目录来处理。

因此ls的实现只需考虑文件和目录两种情况,无需考虑驱动或分区

什么是目录

目录是一种特殊的文件,它的内容是文件和目录的名字。与utmp文件类似,目录文件包含很多记录,每个记录的格式由统一的标准定义。每条记录的内容代表一个文件或目录

与普通文件不同,目录文件永远不会空,每个目录都至少包含两个特殊的项:

  • . 表示当前目录
  • .. 表示上一级目录

可以发现cat more od命令在centos7系统下都无法读取目录

1
2
3
4
5
6
7
8
9
[zhangqi@localhost test]$ cat /
cat: /: Is a directory
[zhangqi@localhost test]$ more /tmp

*** /tmp: directory ***

[zhangqi@localhost test]$ od -c /dev
od: /dev: read error: Is a directory
0000000

那么如何读取呢?

通过 man -k direct | grep read可以找到readdir命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ man 3 readdir

NAME
readdir, readdir_r - read a directory

SYNOPSIS
#include <dirent.h>

struct dirent *readdir(DIR *dirp);

int readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

readdir_r():
_POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _BSD_SOURCE || _SVID_SOURCE || _POSIX_SOURCE

DESCRIPTION
The readdir() function returns a pointer to a dirent structure representing the next directory
entry in the directory stream pointed to by dirp. It returns NULL on reaching the end of the
directory stream or if an error occurred.

On Linux, the dirent structure is defined as follows:

struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* not an offset; see NOTES */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported
by all file system types */
char d_name[256]; /* filename */
};

编写简单的ls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>

extern void do_ls(char *dirname);

int main(int argc, char *argv[]) {
if (argc == 1) {//如果没有任何参数,则显示当前目录下所有文件
do_ls(".");
}
else { //否则,显示指定的目录下的文件,可以加多个参数
while (--argc) {
printf("%s:\n", *++argv);
do_ls(*argv);
}
}
return EXIT_SUCCESS;
}

extern void do_ls(char *dirname) {
struct dirent *dirent_p;
DIR *dir_p = opendir(dirname);
if (dir_p == NULL) {
perror(dirname);
exit(EXIT_FAILURE);
}
while ((dirent_p = readdir(dir_p)) != NULL) {
printf("%s\n", dirent_p->d_name);
}
closedir(dir_p);
return ;
}

这个简单的版本基本实现了查看目录功能,但还需要加进去一些功能:

  • 排序:ls1的输出没有进过排序。
    • 解决办法:把所有文件名读入一个数组,用qsort函数把数组排序
  • 分栏:标准的ls输出是分栏排列的,有些以行排序输出,有些以列排序输出
    • 解决办法:把文件名读入数组,然后计算出列的宽度和行数
  • “.”文件:ls1列出了“.”文件,而标准的ls只有在给出-a选项时才会列出这些文件
    • 解决办法:ls1能够接收选项-a,并在没有-a的时候不显示隐藏文件
  • 选项 -l :如果选项里有-l,标准的ls会列出文件的详细信息,而ls1不会。
    • 解决办法:由于dirent结构中没有所需信息,如文件大小、文件所有者等,要找到存储这些信息的地方(使用stat系统调用)

用stat得到文件信息

用法

1
2
3
4
5
6
7
8
9
10
11
12
NAME
stat, fstat, lstat, fstatat - get file status

SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);

  • stat把文件pathname的信息复制到指针buf所指的结构中
  • 入参:文件名,指向stat结构buffer的指针
  • 返回值:-1(遇到错误) 0(成功返回)

stat结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* file type and mode */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */

/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */

struct timespec st_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last status change */

#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

示例代码:

1
2
3
4
5
6
7
8
9
10
11
//filesize.c
#include <stdio.h>
#include <sys/stat.h>
int main(){
struct stat infobuf;
if(stat("/etc/passwd", &infobuf) == -1)
perror("/etc/passwd");
else
//用stat来获取文件大小
printf("The size of /etc/passwd is %d\n", infobuf.st_size);
}

编写fileinfo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// fileinfo.c - use stat() to obtain and print file properties
// - some menbers are just numbers

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

extern void show_stat_info(char *filename, struct stat *stat_buf);

int main(int argc, char *argv[]) {
struct stat stat_buf;
while (--argc > 0) {
if (stat(*++argv, &stat_buf) == -1) {
perror(*argv);
return EXIT_FAILURE;
}
show_stat_info(*argv, &stat_buf);
}
return EXIT_SUCCESS;
}

extern void show_stat_info(char *filename, struct stat *stat_buf_p) {
printf(" mode: %o\n", stat_buf_p->st_mode);
printf(" links: %d\n", stat_buf_p->st_nlink);
printf(" user: %d\n", stat_buf_p->st_uid);
printf(" group: %d\n", stat_buf_p->st_gid);
printf(" size: %d\n", stat_buf_p->st_size);
printf(" mtime: %d\n", stat_buf_p->st_mtime);
printf(" name: %s\n", filename);
return ;
}

编译,运行,并与ls -l作对比

1
2
3
4
5
6
7
8
9
10
11
[zhangqi@localhost chap3]$ ./fileinfo fileinfo.c 
mode: 100664
links: 1
user: 1000
group: 1000
size: 777
mtime: 1662418390
name: fileinfo.c
[zhangqi@localhost chap3]$ ls -l fileinfo.c
-rw-rw-r--. 1 zhangqi zhangqi 777 Sep 6 06:53 fileinfo.c
[zhangqi@localhost chap3]$

可以发现链接数(links,即文件被引用的次数,即别名的数量)、文件大小(size,即实际所占用的存储空间的字节数)的显示都没有问题,时间是可以用ctime转化成字符串的,也没有问题。

然而fileinfo将模式(mode)字段以数字形式输出,然而需要的是 -rw-rw-r--这种形式。

另外结构中的user和group字段都是数值,显示出来的应该是用户名(需要通过getpwuid()来获取)和组名(通过getgrgid()来获取组列表)。

因此要进一步处理这些问题

st_mode是一个16位的二进制数,文件类型和权限被编码在这个数中:

  • 前四位用作文件类型,最多标识16种类型,目前已使用其中的7个

  • 接下来3位是文件的特殊属性,1代表具有某个属性,0代表没有。

    • set-user-ID位:SUID位告诉内核,运行这个程序的时候认为是由文件所有者在运行这个程序
    • set-group-ID位:用来设置程序运行时所属组
    • sticky位:对于文件来说,告诉内核即使没有人在使用程序,也要把它放在交换空间中(早期类似虚拟内存的实现技术);对于目录来说,sticky位使目录里的文件只能被创建者删除
  • 最后9位是许可权限,分为三组,对应文件所有者、同组用户和其他用户。每组三位,分别是读、写和执行的权限。相应位置如果是1说明有权限,0代表没有

    • 文件所有者:就是创建文件的用户,当用户通过creat建立文件时,内核把文件所有者设为运行程序的用户,如果程序具有set-user-ID位,那么新文件的文件所有者就是程序的文件所有者

    • 组:通常新文件的组被设为执行创建动作的用户所在的组

    • 修改文件所有者和组:通过系统调用chown来修改文件所有者和组:

      chown("file1",200,40);将文件file1的用户ID改为200,组ID改为40,如果后两个参数都为-1,则文件所有者和组都不会改变。

      shell命令chown和chgrp可以用来修改文件所有者和组,它们可以一次修改多个文件

如何读取被编码的值

“掩码”技术——为了比较,把不需要的地方置0,需要的字段值不发生改变

实现:与0作位与(&)操作可以将相应的bit置为0

在<sys/stat.h>中定义了一系列掩码,用于过滤出不同的信息

综合实现

ls2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>

void do_ls(char[]);
void dostat(char*);
void show_file_info(char*, struct stat *);
void mode_to_letters(int, char[]);
char * uid_to_name(uid_t);
char * gid_to_name(gid_t);



main(int argc, char *argv[]) {
if(argc == 1)
do_ls("."); //如果没有指定参数,则列出该目录下所有文件信息
else
while(--argc){
printf("%s:\n",*++argv);
do_ls(*argv);
}
}

void do_ls(char dirname[]){
//list files in directory called dirname
DIR *dir_ptr;
struct dirent * direntp;

if((dir_ptr = opendir(dirname)) == NULL)
fprintf(stderr, "ls2: cannot open %s\n", dirname);
else{
while((direntp = readdir(dir_ptr)) != NULL)
dostat(direntp->d_name);
closedir(dir_ptr);
}
}

void dostat(char * filename){
struct stat info;
if(stat(filename, &info)==-1)
perror(filename);
else
show_file_info(filename,&info);
}
void show_file_info(char *filename, struct stat *info_p) {
char *uid_to_name(), *ctime(), *gid_to_name(), *filemode();
void mode_to_letters();
char modestr[11];

mode_to_letters(info_p->st_mode, modestr);

printf("%s ", modestr);
printf("%4d ",(int)info_p->st_nlink);
printf("%-8s ", uid_to_name(info_p->st_uid));
printf("%-8s ", gid_to_name(info_p->st_gid));
printf("%8ld ", (long)info_p->st_size);
printf("%.12s ", 4+ctime(&info_p->st_mtime));
printf("%s\n", filename);
}

void mode_to_letters(int mode, char str[]){
strcpy(str,"----------");

if(S_ISDIR(mode)) str[0] = 'd';
if(S_ISCHR(mode)) str[0] = 'c';
if(S_ISBLK(mode)) str[0] = 'b';

if(mode & S_IRUSR) str[1] = 'r';
if(mode & S_IWUSR) str[2] = 'w';
if(mode & S_IXUSR) str[3] = 'x';

if(mode & S_IRGRP) str[4] = 'r';
if(mode & S_IWGRP) str[5] = 'w';
if(mode & S_IXGRP) str[6] = 'x';

if(mode & S_IROTH) str[7] = 'r';
if(mode & S_IWOTH) str[8] = 'w';
if(mode & S_IXOTH) str[9] = 'x';
}

#include <pwd.h>
char *uid_to_name(uid_t uid){
struct passwd * getpwuid(), *pw_ptr;
static char numstr[10];

if((pw_ptr = getpwuid(uid))== NULL){
sprintf(numstr,"%d",uid);
return numstr;
}
else
return pw_ptr->pw_name;
}

#include <grp.h>
char *gid_to_name(gid_t gid){
struct group * getgrgid(), *grp_ptr;
static char numstr[10];
if((grp_ptr = getgrgid(gid)) == NULL){
sprintf(numstr,"%d",gid);
return numstr;
}
else
return grp_ptr->gr_name;
}

设置和修改文件的属性

ls2.c的属性

1
2
[zhangqi@localhost chap3]$ ls -l ls2.c
-rw-rw-r--. 1 zhangqi zhangqi 2336 Sep 7 01:11 ls2.c

文件类型

有普通文件、目录文件、设备文件、socket文件、符号链接文件、命名管道文件等

  • 文件类型的建立:是在创建文件的时候建立的,如用系统调用creat建立一个普通文件。其他类型的文件如目录(mkdir()系统调用)等,可使用不同的函数创建。
  • 修改文件类型:文件一经创建,类型就无法修改

许可位与特殊属性位

每个文件都有9位许可权限和3位特殊属性,是在文件创建的时候建立的,创建后,它们可以被chmod系统调用修改

1
2
3
#include <sys/stat.h>

int chmod(const char *path, mode_t mode);
  • 建立文件的模式:creat的第二个参数制定了要创建文件的许可位

    fd = creat("newfile",0744)指定新创建文件的许可位为rwxr–r–

  • 改变文件的模式:

    • 程序可以通过系统调用chmod来改变文件的模式:

      chmod("/tmp/myfile", 04764);

      chomod("/tmp/myfile,S_ISUID|S_IRWXU|S_IRWXU|S_IRGRP|S_IWGRP|S_IROTH");

    • 以上两条指令的作用相同,第一条是八进制表示,第二条是用<sys/stat.h>中定义的符号来表示。后者有明显的优点,当系统定义的许可位的值改变时,无需修改程序。即系统调用chmod不受“新建文件掩码”的影响

    • 返回-1即错误,0则是成功

    • shell命令chmod也可以用来完成上述操作。它可以通过八进制模式(如04764)和符号模式(如u=rws g=rw o=r)来指定权限和属性

文件系统:编写pwd

文件包含数据,而目录是文件的列表。不同的目录互相连接构成树状的结构。目录还可以包含其他的目录。

pwd命令显示你在目录树中的位置。从树根到你所处位置所经过的目录的序列被称作路径(path)。

用户角度看文件系统

Unix系统中一盘上的文件组成一棵目录树

针对目录树的命令:

  • ls -R可以查看以当前节点为根节点的整个树结构。-R要求列出指定目录及其子目录的所有内容
  • chmod -R 可以改变子目录中所有文件的许可权
  • du 是disk usage的缩写,该命令给出指定目录及其子目录下所有文件占用硬盘中数据块的总数
  • find 将在一个目录及其所有子目录中检索符合要求的文件和目录

目录树的深度几乎没有限制

Unix文件系统的内部结构

硬盘实际上是由一些磁性盘片组成的计算机系统的一个设备。文件系统其实是对该设备的一种多层次的抽象

第一层抽象:从磁盘到分区

  • 一个磁盘可被划分为若干个分区,以便在一个大的实体内创建独立的区域。每个分区都可以看做是一个独立的磁盘

第二层抽象:从磁盘到块序列

  • 一个磁盘由一些磁性盘片组成。每个盘片的表面都被划分为很多同心圆,这些同心圆被称作磁道,每个磁道又进一步被划分为扇区。

  • 每个扇区可以存储一定字节数的数据(512字节)

  • 扇区是磁盘上的基本存储单元

  • 为磁盘块编号:

    • 给每个磁盘块分配连续的编号使得系统能够计算磁盘上的每个块。
    • 可以一个磁盘接一个磁盘从上到下给所有块编号,还可以一个磁道接一个磁道地从外向里给所有的块编号。
    • 一个将磁盘扇区编号的系统使得我们可以把磁盘视为一系列块的组合

第三层抽象:从块序列到三个区域的划分

  • 文件系统被分为三个区域来存储不同类型的数据(文件内容、文件属性(所有者、日期等)和目录)

    • 超级块:存放文件系统本身的信息(如每个区域的大小、未被使用的磁盘块信息)
    • i-节点表:存放文件属性(如大小、文件所有者和最近修改时间)
      • 这些性质被记录在一个称为 “i-节点”的结构中,所有inode有相同的大小,这是一个inode的列表。
      • 文件系统中的每个文件在该表中都有一个inode
      • 如果有root权限,可以像操作文件一样将分区打开、阅读并显示i-节点表。
      • 注意:表中的每个inode都通过位置来标识。例如,标识为2的inode(inode 2)位于文件系统i-节点表中的第3个位置
    • 数据区:用来存放文件内容。一个较大的文件可能分布在上千个独立的磁盘块中
  • 文件系统由这三部分组合而成,其中任一部分都是由很多有序磁盘块组成的

文件系统的实现:

创建一个文件的过程

考虑如下命令: who > userlist,创建了一个存放命令who输出内容的新文件

1
2
3
4
5
6
7
[zhangqi@localhost chap4]$ who > userlist
[zhangqi@localhost chap4]$ ls
demodir userlist
[zhangqi@localhost chap4]$ cat userlist
zhangqi :0 2022-09-06 06:39 (:0)
zhangqi pts/0 2022-09-06 06:46 (:0)
zhangqi pts/1 2022-09-08 08:13 (:0)

文件有内容和属性,内核将文件内容存放在数据区,文件属性存放在inode节点,文件名存在目录

创建一个新文件的四个操作:

  • 存储属性:
    • 文件属性的存储:内核先找到一个空的 inode。如图中,内核找到 inode 47。内核把文件的信息记录其中
  • 存储数据
    • 文件内容的存储:由于该新文件需要3个存储磁盘块,因此内核从自由块的列表中找出3个自由块。如图中,找到块627、200和992。内核将第一块数据复制到块627, 下一块数据复制到块200,最后一块数据复制到块992。
  • 记录分配情况:
    • 内核在inode的磁盘分布区记录了上述的块序列。
    • 磁盘分布区是一个磁盘块序号的列表,这3个编号放在最开始的3个位置
  • 添加文件名到目录
    • 新文件的名字是userlist。Unix内核将入口(47,userlist)添加到目录文件。
    • 文件名和i-节点号之间的对应关系将文件名和文件内容及属性连接了起来

目录的工作过程

目录是一种包含了文件名字列表的特殊文件。不同版本的Unix目录的内部结构不同,但是抽象模型总是一致的——一个包含i-节点号和文件名的表

可通过 ls -1ia (选项第一位是数字1)来看目录的内容:

  • 输出的是文件名和对应的 i-节点号,如文件名x对应于 i-节点号37405777
  • 当前目录用 .表示,有关大小、文件所有者、组等各项关于当前目录的信息存放在i-节点表中的编号为37405773中
  • -i告诉ls在列表中包含 i-节点号
  • -1要求每行列出一个文件
1
2
3
4
5
6
7
8
9
10
11
12
[zhangqi@localhost chap4]$ ls -1ia
17507719 .
36866887 ..
37405773 demodir
17507727 userlist
[zhangqi@localhost chap4]$ ls -1ia demodir
37405773 .
17507719 ..
51681516 a
51681478 c
37405778 copy.of.x
37405777 x

可以用 ls -i查看系统上任何一个文件的 i-节点号。

如查看系统根目录中各文件:

1
2
3
4
5
[zhangqi@localhost chap4]$ ls -ia /
64 . 3 dev 83 lib64 1 proc 551128 snap 33593488 usr
64 .. 16777281 etc 84 media 33574977 root 50332836 srv 50331713 var
74275 bin 50332835 home 16777863 mnt 8868 run 1 sys
64 boot 81 lib 33593545 opt 74279 sbin 16777288 tmp

这个列表有两个重要的注意点:

  • procsys文件的节点号相同都为1,这说明他们是同一个文件的两个不同名字
  • 根目录的...都指向同一个目录。根目录比较特别,当Unix命令mkfs创建一个文件系统,mkfs将根目录的父目录指向自己

cat命令的工作原理

cat userlist 从目录文件到找到数据:

  1. 在目录寻找文件名:

    • 内核在目录文件中寻找包含字符串userlist的记录。从而找到编号为47的i-节点号
  2. 定位 i-节点 47 并读取其内容:

    • 内核在文件系统中的i-节点区域找到节点 inode 47。(由于所有inode大小相同,每个磁盘块包含相同数量的inode,因此可通过简单的计算进行定位)。
    • 为了提高访问效率,内核有可能将inode置于缓冲区中。inode包含数据块编号的列表
  3. 访问存储文件内容的数据块

    • 通过以上过程,内核已经可以知道文件内容存放在哪些数据块上,以及它们的顺序。由于cat不断地调用read函数,使得内核不断将字节从磁盘复制到内核缓冲区,进而到达用户空间
    • 所有从文件读取数据的命令,例如cat、cp、more、who等,都是将文件名传给open来访问文件内容。对open的每次调用都是先在目录中寻找文件名,然后根据目录中的i-节点号获得文件的属性,最终找到文件的内容

如果open一个没有读或写权限的文件:

  • 内核首先完成1 2两步
  • 然后在inode中内核可以读取文件权限相关的信息(权限位和拥有者的用户ID)
  • 如果权限位设置你的用户ID对文件没有访问权限,则open返回-1并且将全局变量errno的值设为EPERM

如何跟踪大文件

我们知道一个大的文件需要多个磁盘块,而inode中存放有磁盘块分配列表。那么一个固定大小的inode如何存储较长的分配列表呢?

如图所示,这个文件需要14个数据块存储它的内容,因此,分配链表包含14个块的编号。但是文件的inode只包含一个含有13个项的分配链表。那么应该如何存放呢?

解决方案:将分配链表的前10个编号放到inode中,将最后4个编号放到一个数据块中:

  • 具体来说,就是该inode的链表包含分配13个块编号的空间,链表里的前10个项中的块编号指向的是文件的实际数据。
  • 如果分配链表有多余10个的项,则剩下的块编号不是存储在inode,而是存储在数据区。
  • 链表的第11项存储了存放多余编号的数据块的编号
  • 如果仍然不够,链表的第12项(二级间接块)存放那个存储着第2、3、4及后继额外块的编号的块的编号
  • 还是不够,使用第13项(三级间接块),加深一层递归
  • 此时文件大小达到了极限,还要更大的空间就需要一个拥有更大的磁盘块构成的文件系统了。

Unix文件系统的改进

不同版本的Unix使用前述文件系统模型的不同版本。这种方法很简洁,但存在一些问题:

  • 超级块如果损坏了,则整个文件系统的结构信息就没有了。因此新版本的Unix在文件系统中备份了这个块的副本
  • 分块问题。由于文件的创建和删除,自由块将遍布磁盘。一种方案是在文件系统中创建被称为柱面组(cylinder group)的微文件系统

理解目录

“x文件在目录a中”的含义:

从系统角度看,目录a中有一个指向 文件x对应的i-节点号的链接(xLink),这个链接附加的文件名为x

“目录demodir包含子目录a”的含义:

demodir包含一个指向子目录a对应的inode的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zhangqi@localhost chap4]$ ls -iaR demodir/
demodir/:
37405773 . 17507719 .. 51681516 a 51681478 c 37405778 copy.of.x 37405777 x

demodir/a:
51681516 . 37405773 .. 51681517 x 51681518 y

demodir/c:
51681478 . 37405773 .. 17507718 d1 37405776 d2

demodir/c/d1:
17507718 . 51681478 .. 51681517 xlink

demodir/c/d2:
37405776 . 51681478 .. 37405775 xcopy

“目录a有一个父目录demodir”的含义:

目录a中有一个 ..的名字,为父目录的保留名字

多重链接及链接数

在demodir目录树中,inode 51681517有两个链接,一个在目录a中,称为x,另一个在目录d1中,称为xlink。那么哪个是原始文件,那个是指向它的链接呢?

在Unix的目录结构中,这两个链接的状态完全相同;它们被称为指向文件的硬链接。

文件是一个inode和一些数据块的结合;链接是对inode的引用。可以对一个文件创建任意多的链接

内核记录了一个文件的链接数,就inode 51681517来说,链接数至少是2。因为在文件系统的其他部分或许还存在着这个文件的其他链接。

链接数被记录在inode中,同时也是系统调用stat返回值stat结构中的一个成员

与目录树相关的命令和系统调用

mkdir

用于创建新的目录。它接受命令行上的一个或多个目录名,使用mkdir(2)系统调用.

1
2
3
4
#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname, mode_t mode);

实际上mkdir创建了一个新的目录节点并把它链接至文件系统树:

  • mkdir创建了这个目录的inode
  • 分配了一个磁盘块用以存储它的内容
  • 在目录中设置了两个入口: ...并正确配置了它们的 i-节点号
  • 在它的父目录中增加一个该节点的链接

rmdir

用于删除一个目录,使用rmdir(2)系统调用

1
2
3
#include <unistd.h>

int rmdir(const char *pathname);

rmdir从目录树中删除一个目录节点。这个目录必须是空的。即除了 ...的入口,这个目录不能包含其他任何的文件和子目录。同时在父目录中删除这个目录的链接,如果这个目录本身并未被其他进程占用,它的inode和数据块将被释放

rm

用来从一个目录文件中删除一条记录,接受命令行上一个或多个文件名,使用unlink(2)系统调用

1
2
3
#include <unistd.h>

int unlink(const char *pathname);

unlink用于删除目录文件中的一个记录,减少相应inode的链接数。如果该inode的链接数减为0,数据块和inode将被释放(和C++中shared_ptr的设计思想很像,妙)。unlink不能用于删除目录

ln

用来创建一个文件的链接,使用系统调用link(2)

1
2
3
#include <unistd.h>

int link(const char *oldpath, const char *newpath);

link不能用来生成目录的新链接

mv

用来改变文件和目录的名字或位置。很多情况下mv仅仅使用系统调用rename

1
2
3
#include <stdio.h>

int rename(const char *oldpath, const char *newpath);

当入参位于同一个目录时,即实现了重命名

当入参位于不同的目录时,即实现了移动,由于实际上目录中存放的是链接,因此就是把链接从一个目录移动到另一个目录:

  • 复制链接至新的名字/位置(调用link())
  • 删除原来的链接(调用unlink())

实际上过去没有rename这个系统调用,而是结合link和unlink使用,新增的目的是:

  • rename使重命名或重定位一个目录变得更加安全
  • 可以支持非Unix系统。将通用方法rename添加至内核隐藏了实现的细节,使相同的代码能够在各种文件系统上运行

cd

用来改变进程的当前目录。cd对进程产生影响,但是并不影响目录。

cd使用系统调用chdir()

1
2
3
#include <unistd.h>

int chdir(const char *path);

Unix上的每个运行程序都有一个当前目录,chdir系统调用改变进程的当前目录。在系统内部,进程有一个存放当前目录 i-节点号的变量。从一个目录进入另一个目录只是改变那个变量的值

编写pwd

命令pwd用来显式到达当前目录的路径

1
2
[zhangqi@localhost chap4]$ pwd
/home/zhangqi/ZZQ/test/chap4

这个路径并不位于当前的目录中,当前目录称呼其本身为. ,并且有一个 i-节点号。

因此要获得完整的路径名,要追踪链接,读取目录,一个目录接着一个目录地沿着树向上追踪,每步查看 .的节点号(自身的节点号),然后在父目录中查找该 inode的名字,直到到达树的顶端。

伪代码:

while(没有到达树的顶端){

​ 得到 .的i-节点号,称其为n(使用stat)

​ chdir.. (使用chdir)

​ 找到 i-节点号 n链接的名字(使用opendir、readdir 、closedir)

}

如何判断是否达到树的顶端:判断直到一个目录的 ...的 i-节点号相同时

如何以证券顺序显示目录名字:建立一个循环,使用strcat或sprintf建立目录名字的字符串序列。通过一个递归的程序逐步到达树的顶端来一个接一个地显示目录名,从而避免了字符串的管理

pwd的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// spwd.c: a simplified version of pwd
//
// starts in current directory and recursively
// climbs up to root of filesystem, prints top part
// then prints current part
//
// uses readdir() to get info about each thing

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>

extern ino_t get_ino(char *dir);
extern void printpath(ino_t this_inode);
extern void inode_num_to_name(ino_t this_ino, char this_inode_name[BUFSIZ]);

int main(int argc, char *argv[]) {
int ino = get_ino(".");
if (ino == 2) {
printf("/\n");
return EXIT_SUCCESS;
}
printpath(ino);
printf("\n");
return EXIT_SUCCESS;
}

// prints path leadin down to an object with this inode
// kindof recursive

extern void printpath(ino_t this_ino) {
char this_inode_name[BUFSIZ];
ino_t father_inode_num = get_ino("..");
if (father_inode_num != this_ino) {
inode_num_to_name(this_ino, this_inode_name);
chdir("..");
printpath(father_inode_num);
printf("/%s", this_inode_name);
}
}

// returns inode number of the file
extern ino_t get_ino(char *dir) {
struct stat info;
if (stat(dir, &info) == -1) {
fprintf(stderr, "Cannot stat ");
perror(dir);
exit(EXIT_FAILURE);
}
return info.st_ino;
}

// looks through current directory for a file with this inode
// number and copies its name into namebuf

extern void inode_num_to_name(ino_t this_ino, char this_inode_name[]) {
struct dirent *dirent_p;
DIR *dir_p = opendir("..");
if (dir_p == NULL) {
perror("..");
exit(EXIT_FAILURE);
}
// search directory for a file with specified inum
while ((dirent_p = readdir(dir_p)) != NULL) {
if (dirent_p->d_ino == this_ino) {
strcpy(this_inode_name, dirent_p->d_name);
closedir(dir_p);
return ;
}
}
fprintf(stderr, "error looking from inum %ld\n", this_ino);
exit(EXIT_FAILURE);
}

连接控制:学习stty

计算机除了文件和目录相关的程序,还有其他的数据来源,如调制解调器、打印机、扫描仪、鼠标、扬声器、照相机和终端等这样的设备。

设备的读写方法:对Unix来说,声卡,终端,鼠标和磁盘文件是同一种对象。在Unix系统中,每个设备都被当做一个文件。每个设备都有一个文件名,一个i-节点号、一个文件所有者、一个权限位的集合和最近修改时间。

和文件有关的所有内容都将被运用于终端和其他设备

设备具有文件名

通常表示设备的文件存放在目录/dev中(devices),但是可以在任何目录中创建设备文件。

查看我的机器上的/dev目录:

1
2
3
4
5
6
$ ls -C /dev | head -5
agpgart kmsg rtc tty2 tty44 ttyS2
autofs log rtc0 tty20 tty45 ttyS3
block loop0 sda tty21 tty46 uhid
bsg loop1 sda1 tty22 tty47 uinput
btrfs-control loop2 sda2 tty23 tty48 urandom

相关系统调用

设备不仅具有文件名,还支持所有与文件相关的系统调用:open、read、write、lseek、close和stat

实际上Unix没有其他的方法用来和设备通信。

设备文件的属性

(这部分没有太理解,暂时简单记录并略过)

1
2
$ ls -li /dev/pts/1
4 crw--w----. 1 zhangqi tty 136, 1 Sep 10 2022 /dev/pts/1

设备文件具有磁盘文件的大部分属性。如上面ls的输出内容表明/dev/pts/1 拥有inode 4,权限位为rw–w—-,1个链接,文件所有者zhangqi和组tty,最近修改时间为Sep 10 2022。文件类型为“c”,表示这个文件实际上是以字符为单位进行传送的设备

常用的磁盘文件中的字节数就是文件的大小。但设备文件是链接而不是容器。键盘和鼠标不存储击键数和点击数。

设备文件的inode存储的是指向内核子程序的指针,而不是文件的大小和存储列表。

内核中传输设备数据的子程序被称为设备驱动程序。

在/dev/pts/1中,参数是1。136和1这两个数被称为设备的主设备号和从设备号:

  • 主设备号确定处理该设备实际的子程序
  • 从设备号被作为参数传输到该子程序

设备文件的读写执行权限:

当文件实际上表示设备时

  • 向文件写入数据就是把数据发送到设备,权限写意味着允许发送数据
  • 读文件就是从文件获得数据

Unix文件系统的inode和数据块是如何支持设备文件的概念的:

  • 目录并不能区分哪些文件名代表磁盘文件,哪些文件名代表设备
  • 文件类型的区别体现在inode上:
    • 每个inode的类型被记录在结构stat的成员变量st_mode的类型区域中
    • 磁盘文件的inode包含指向数据块的指针。设备文件的inode包含指向内核子程序表的指针
    • 主设备号用于告知从设备读取数据的那部分代码的位置
  • 考虑read的工作流程(其他如open、write、lseek和close等都是类似的):
    • 内核首先找到文件描述符fd的inode,该inode用于告诉内核文件的类型
    • 如果文件是磁盘文件,那么内核通过访问块分配表来读取数据
    • 如果文件是设备文件,那么内核通过调用该设备驱动程序的read部分来读取数据

设备和文件的不同之处

系统通过调用open创建文件和设备与进程的连接,但是他们有着不同的性质。
磁盘连接的两个主要属性:

  • 缓冲:磁盘文件有缓冲区,可以通过fcntl()关闭
  • 自动添加模式:即当文件描述符的O_APPEND打开后,每个对write的调用自动调用lseek将内容添加到文件末尾。

终端连接
具有回显,波特率, 编辑和换行会话。

终端
终端是人们用来和unix进程进行通信的设备。终端拥有一个可以让进程读取字符的键盘和可让进程发送字符的显示器。
进程与终端间的数据传输和数据处理由终端驱动程序负责,终端驱动程序是内核的一部分,该部分代码提供缓冲,编辑和数据转换

stty命令

stty命令让用户读取和修改终端驱动程序的设置

使用stty显示驱动程序设置:

1
2
3
4
$ stty
speed 38400 baud; line = 0;
eol = M-^?; eol2 = M-^?; swtch = M-^?;
ixany iutf8

使用stty改变驱动程序属性

1
2
3
$ stty erase X     #将删除键改为X
$ stty - echo #关闭按键显示,即输入的字符不显示在屏幕上
$ stty erase @ echo #复合命令:将删除键改为@,并打开按键显示

编写终端驱动程序

关于设置

tty驱动程序包含很多对传入的数据所进行的操作。这些操作分为4种:

  • 输入:如何处理从终端来的字符。包括将小写转换为大写,去除最高位及将回车符转换为换行符
  • 输出:如何处理流向终端的字符。用若干个空格符代替制表符,将换行符转换为回车符及将小写字母转换为大写字母
  • 控制:字符如何被表示——位的个数、位的奇偶性、停止位等
  • 本地:如何处理来自驱动程序内部的字符。包括回显字符给用户及缓冲输入直到用户按回车键

关于函数

改变终端驱动程序的设置就像改变磁盘文件连接的设置一样:

  • 从驱动程序获得属性
  • 修改所要修改的属性
  • 将修改过的属性送回驱动程序

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

1
2
3
4
5
#include <termios.h>
struct termios settings; //struct to hold attributes
tcgetattr(fd,&settings); //get attribs from driver
settings.c_lflag | = ECHO; //turn on ECHO bit in flagset
tcsetattr(fd, TCSANOW, &settings); //send attribs back to driver

相关系统调用

  • tcsetattr:设置tty驱动程序的属性,遇到错误返回-1,成功返回0
    • optional_actions参数告诉tcsetsttr在什么时候更新驱动程序
    • TCSANOW:立即更新驱动程序设置
    • TCSADRAIN:等待直到驱动程序队列中的所有输出都被传送到终端。然后更新
    • TCSAFLUSH:等待直到驱动程序队列中的所有输出都被传送出去。然后,释放所有队列中的输入数据,并进行一定的变化
  • tcgetattr:读取tty驱动程序的属性
1
2
3
4
5
6
7
#include <termios.h>
#include <unistd.h>

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions,
const struct termios *termios_p);

termios的结构(包含若干个标志集和一个控制字符的数组)和位:

1
2
3
4
5
6
7
8
9
10
The termios structure
Many of the functions described here have a termios_p argument that is
a pointer to a termios structure. This structure contains at least the
following members:

tcflag_t c_iflag; /* input modes */
tcflag_t c_oflag; /* output modes */
tcflag_t c_cflag; /* control modes */
tcflag_t c_lflag; /* local modes */
cc_t c_cc[NCCS]; /* special characters */

每个标志集的独立位的含义:(前四个为标志集,c_cc为控制字符的数组)

每个属性在标志集中都占有一位。属性的掩码定义在termios.h中。

其他设备编程

  • fcntl:控制文件描述符

  • ioctl:控制一个设备,request为需进行的操作,…为操作所需参数

    1
    2
    3
    #include <sys/ioctl.h>

    int ioctl(int fd, int request, ...);

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

软件工具:对磁盘文件和设备文件不加以区分的程序(Unix中有好几百个,如who、ls、sort等)

  • 从标准输入stdin或文件读取字节,进行一些处理,然后将包含结果的字节流写到标准输出stdout

  • 工具发送错误消息到标准错误输出,它们也被当作简单的字节流来处理。

  • 这些文件描述符能够连接到文件、终端、鼠标、光电管、打印机等

  • 工具对所处理的数据的源和目的地不做任何假设

  • 其他很多程序也能从命令行所指定的文件中读取数据

  • 这些程序的输入和输出能够被重定向到任何类型的连接上:

    1
    2
    3
    $ sort > outputfile
    $ sort x > /dev/lp
    $ who | tr '[a - z]' '[A - Z]'

特定设备程序:为特定应用控制设备

其他程序(如终端、控制扫描仪、记录压缩盘、操作磁带驱动程序)也能同特定设备进行交互。

用户程序:一种常见的设备相关程序

如vi、emacs、pine、more等

这些程序设置终端驱动程序的击键和输出处理方式。驱动程序有很多设置,但是用户常用到的有:

  1. 立即响应击键事件
  2. 有限的输入集
  3. 输入的超时
  4. 屏蔽Ctrl-C

下面将编写一个实现所有这些特点的程序

终端驱动程序的模式

规范处理

首先看一个简短的转换程序(将输入的字符串的所有字符改为该字符的下一个)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <ctype.h>

int main(){
int c;
while((c = getchar())!=EOF){
if(c=='z') c = 'a';
else if(islower(c)) ++c;
putchar(c);
}
return 0;
}

使用默认设置运行这个程序(<- 是退格键)

1
2
3
4
5
6
7
$ cc rotate.c -o rotate
$ ./rotate
abx<-cd
bcde

efgCtrl-C
$

上述结果揭示了标准输入处理的特征:

  • 程序未得到输入的“x”,因为退格键删除了它
  • 击键的同时字符显示在屏幕上,但是直到按了回车键,程序才接收到输入
  • Ctrl-C键结束输入并终止程序

程序rotate不做这些操作。缓冲、回显、编辑和控制键处理都由驱动程序完成:

缓冲和编辑包含规范处理(canonical rpocessing)。当这些特征被启动,终端连接被称为处于规范模式(cooked模式)。

非规范处理

命令 stty -icanon会关闭驱动程序的规范模式处理。非规范模式没有缓冲,因此输入一个字母就会直接显示。当试图删除一个字符,驱动程序不能做任何事情;字符早就送给程序了

1
2
3
4
[zhangqi@localhost chap6]$ stty -icanon
[zhangqi@localhost chap6]$ ./rotate
abstdefg~~??^?^?^?^C
[zhangqi@localhost chap6]$ stty icanon

另外也可以通过 stty -icanon -echo同时关闭规范模式和回显模式

raw模式

当所有处理都被关闭后,驱动程序将输入直接原封不动传递给程序。这种情况下驱动程序被称为处于raw模式、

编写play_again.c

逻辑:

  • 对用户显示提示问题
  • 接受输入
  • 如果是“y”,返回0
  • 如果是“n”,返回1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// purpose: ask if user wants another transaction
// method: ask a quesion, wait for yes/no anser
// returns: 0=>yes, 1=>no
// better: eliminate need to press return

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

#define QUESTION "Do you want another transaction"

extern int get_response(char *);

int main(int argc, char *argv[]) {
int response;
response = get_response(QUESTION);
return response;
}

extern int get_response(char *question) {
int c;
printf("%s (y/n)?", question);
while (true) {
switch(c = getchar()) {
case 'y':
case 'Y': return 0;
case 'n':
case 'N':
case EOF: return 1;
// no default, ignore all other input
}
}
}

这个程序显示提问,然后循环读取输入,直到用户输入“y”、“n”、“Y”或“N”才停止。

有两个问题,都是由于运行时处在规范模式引起的:

  • 用户必须按回车键,play_again0才能接收到数据
  • 当用户按回车键时,接收整行数据并对其进行处理

改进1:即时响应

因此第一个改进就是关闭规范输入,使程序能在用户敲键的同时得到输入的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// play_again1.c
// purpose: ask if user wants another transaction
// method: set tty into char-by-char mode, read char, return result
// returns: 0=>yes, 1=>no
// better: do no echo inappropriate input

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

#define QUESTION "Do you want another transaction"

extern void tty_mode(int how);
extern void set_crmode(void);
extern int get_response(char *);

int main(int argc, char *argv[]) {
tty_mode(0); // save tty mode
set_crmode();
int response = get_response(QUESTION);
tty_mode(1); // reload tty mode
return response;
}

extern void tty_mode(int how) {
static struct termios original_mode;
if (how == 0) {
tcgetattr(0, &original_mode);
}
else {
tcsetattr(0, TCSANOW, &original_mode);
}
}

extern void set_crmode(void) {
struct termios ttystate;
tcgetattr(0, &ttystate);
ttystate.c_lflag &= ~ICANON; // no buffering
ttystate.c_cc[VMIN] = 1; // get one char at a time
tcsetattr(0, TCSANOW, &ttystate);
}

extern int get_response(char *question) {
printf("%s (y/n)?", question);
while (true) {
char c = getchar();
switch(c) {
case 'y':
case 'Y': return 0;
case 'n':
case 'N':
case EOF: return 1;
default:
printf("\ncannot understand %c, ", c);
printf("Please type y or n\n");
}
}
}

play_again1首先将终端置于一个char-by-char模式,然后调用函数显示一个提示符,并获得一个响应,最后设置终端为原始的模式。

注意:最后并未将终端置于桂丹模式。取而代之的是,将原先的设置复制到一个称为original_mode的结构中,结束时恢复这些设置。

改进2:忽略非法键

上述实现对每个非法字符都提示错误信息。因此更好的设计时关闭回显模式,丢掉不需要的字符,直到得到可接受的字符为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// play_again2.c
// purpose: ask if user wants another transaction
// method: set tty into char-by-char mode and no -echo mode
// read char, return result
// returns: 0=>yes, 1=>no
// better: time out if user walks away

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

#define QUESTION "Do you want another transaction"

extern void tty_mode(int how);
extern void set_cr_noecho_mode(void);
extern int get_response(char *);

int main(int argc, char *argv[]) {
tty_mode(0); // save tty mode
set_cr_noecho_mode();
int response = get_response(QUESTION);
tty_mode(1); // reload tty mode
return response;
}

extern void tty_mode(int how) {
static struct termios original_mode;
if (how == 0) {
tcgetattr(0, &original_mode);
}
else {
tcsetattr(0, TCSANOW, &original_mode);
}
}

extern void set_cr_noecho_mode(void) {
struct termios ttystate;
tcgetattr(0, &ttystate);
ttystate.c_lflag &= ~ICANON; // no buffering
ttystate.c_lflag &= ~ECHO; // no echo either
ttystate.c_cc[VMIN] = 1; // get one char at a time
tcsetattr(0, TCSANOW, &ttystate);
}

extern int get_response(char *question) {
printf("%s (y/n)?", question);
while (true) {
char c = getchar();
switch(c) {
case 'y':
case 'Y':
putchar('y');
return 0;
case 'n':
case 'N':
case EOF:
putchar('n');
return 1;
}
}
}

改进3:非阻塞模式实现超时响应

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

  • 当调用getchar或read从文件描述符读取输入时,这些调用通常会等待输入。
  • 在play_again例子中,对getchar的调用使得程序一直等待用户的输入,直到用户输入一个字符。
  • 程序被阻塞,直到能获得某些字符或是检测到了文件的末尾

如何关闭阻塞:

  • 阻塞不仅仅是终端连接的属性,而是任何一个打开的文件的属性

  • 程序可以使用fcntl或open为文件描述符启动非阻塞输入(nonblock input)

    1
    2
    3
    4
    5
    6
    7
    8
    #include <unistd.h>
    #include <fcntl.h>

    int fcntl(int fd, int cmd, ... /* arg */ );

    // fcntl() performs one of the operations described below on the open file descriptor fd. The operation is determined by cmd.
    // fcntl() can take an optional third argument. Whether or not this argument is required is determined by cmd. The required argument type is indicated in parentheses after each cmd name (in most cases, the required type is int, and we identify the argument using the name arg), or void is specified if the argument is not required.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    File descriptor flags
    The following commands manipulate the flags associated with a file descriptor. Currently,
    only one such flag is defined: FD_CLOEXEC, the close-on-exec flag. If the FD_CLOEXEC bit is
    0, the file descriptor will remain open across an execve(2), otherwise it will be closed.

    F_GETFD (void)
    Read the file descriptor flags; arg is ignored.

    F_SETFD (int)
    Set the file descriptor flags to the value specified by arg.

    File status flags
    Each open file description has certain associated status flags, initialized by open(2) and
    possibly modified by fcntl(). Duplicated file descriptors (made with dup(2), fcntl(F_DUPFD),
    fork(2), etc.) refer to the same open file description, and thus share the same file status
    flags.

    The file status flags and their semantics are described in open(2).

    F_GETFL (void)
    Get the file access mode and the file status flags; arg is ignored.

    F_SETFL (int)
    Set the file status flags to the value specified by arg. File access mode (O_RDONLY,
    O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) in
    arg are ignored. On Linux this command can change only the O_APPEND, O_ASYNC,
    O_DIRECT, O_NOATIME, and O_NONBLOCK flags.

  • 可传入O_NDELAY或O_NONBLOCK开启非阻塞模式

  • 非阻塞操作的内部实现很简单:每个文件都有一块保存未读取数据的地方。如果文件描述符置了O_NDELAY位,并且那块空间是空的,read调用返回0.可以参考O_NDELAY有关的源代码深入了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// play_again3.c
// purpose: ask if user wants another transaction
// method: set tty into char-by-char mode, no -echo mdoe
// set tty into no -delay mode
// read char, return result
// returns: 0=>yes, 1=>no, 2=>timeout
// better: reset terminal mode on Interrupt

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

#define QUESTION "Do you want another transaction"
#define TRIES 3
#define SLEEPTIME 2
#define BEEP putchar('\a')

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

int maxtries = TRIES; // 教材版源代码实际上漏掉了该变量的声明

int main(int argc, char *argv[]) {
tty_mode(0); // save tty mode
set_nodelay_mode();
set_cr_noecho_mode();
int response = get_response(QUESTION);
tty_mode(1); // reload tty mode
return response;
}

extern void tty_mode(int how) {
static struct termios original_mode;
if (how == 0) {
tcgetattr(0, &original_mode);
}
else {
tcsetattr(0, TCSANOW, &original_mode);
}
}

extern void set_nodelay_mode(void) {
int termflags;
termflags = fcntl(0, F_GETFL);
termflags |= O_NDELAY;
fcntl(0, F_SETFL, termflags);
}

extern void set_cr_noecho_mode(void) {
struct termios ttystate;
tcgetattr(0, &ttystate);
ttystate.c_lflag &= ~ICANON; // no buffering
ttystate.c_lflag &= ~ECHO; // no echo either
ttystate.c_cc[VMIN] = 1; // get one char at a time
tcsetattr(0, TCSANOW, &ttystate);
}

extern int get_ok_char(void) {
int c;
while ((c = getchar()) != EOF && strchr("yYnN", c) == NULL) {
;
}
return c;
}

extern int get_response(char *question) {
int c;
printf("%s (y/n)?", question);
fflush(stdout);
while (true) {
sleep(SLEEPTIME);
c = tolower(get_ok_char());
if (c != EOF)
putchar(c);
if (c == 'y')
return 0;
if (c == 'n')
return 1;
if (maxtries-- == 0)
return 2;
BEEP;
}
}

这个版本的程序使用fcntl关闭和开启非阻塞模式,其次在get_response中使用aleep和计数器maxtries

小问题:

  • 程序会在调用getchar给用户输入字符之前睡眠2s。就算用户在1s内完成输入,程序也要在2s后才得到字符,用户体验不好
    • 为了使程序更快响应,可以减少每次调用getchar间的睡眠时间,并相应增加循环次数来实现相同的超时设置
  • 另外注意在显示提示符之后对fflush的调用。如果没有这一行,在调用getchar之前,提示符将不能显示。原因是终端驱动程序不仅一行行地缓冲输入,还一行行地缓冲输出。驱动程序缓冲输出,直到它收到一个换行符或者程序试图从终端读取输入

实现超时的其他方法:

Unix提供更好的方法来实现超时功能,在驱动程序中设置数组c_cc[]中的元素VTIME将超时功能的实现移至终端驱动程序。系统调用select包含一个超时参数。

还有一个大问题(如果不是bash等中断,可能会):

当按下Ctrl-C终止程序play-again时,会导致程序无法执行重置驱动程序的代码。

信号

Ctrl-C中断当前运行的程序。这个中断由一个称为信号的内核机制产生。

中断信号的击键组合不一定非是Ctrl-C,可以使用stty(或tcsetattr)将当前的VINTR控制字符替换成另一种键

信号是什么

信号是由单个词组成的消息。当按Ctrl-C时,内核向当前正在运行的进程发送中断信号。

每个信号都有一个数字编码。中断信号通常是编码2.

生成信号的请求来自三个地方:

  • 用户。用户通过输入Ctrl-C、Ctrl-\,或是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号
  • 内核。当进程执行出错时,内核给进程发送一个信号,例如,非法段存取、浮点数溢出,或是一个非法的机器指令。内核也利用信号通知进程特定事件的发生
  • 进程。一个进程可以通过系统调用kill给另一个进程发送信号。一个进程可以和另一个进程通过信号通信。

由进程的某个操作产生的信号被同步信号(Synchronous Signals),例如,被0除。

由用户击键这样的进程外时间引起的信号被称为异步信号(Asynchronous Aignals)。

信号编号以及它们的名字通常出现在 /usr/include/signal.h文件中

通过查看signal(7):

image-20220911015332382

中断信号被称为SIGINT,退出信号被称为SIGQUIT,非法段存取信号是SIGSEGV。

进程如何处理信号

当接收到SIGINT时,并不一定非要消亡。进程能够通过系统调用signal告诉内核,它要如何处理信号,有三个选择:

  • 接受默认处理(通常是消亡)

    • 手册上列出了对每个信号的默认处理。SIGINT的默认处理是消亡。
    • 进程并不一定要使用signal接受默认处理,但是能通过调用signal(SIGINT, SIG_DFL);来恢复默认处理
  • 忽略信号

    • 可通过 signal(SIGINT, SIG_IGN);来告诉内核,它需要忽略SIGINT信号
  • 调用一个函数

    • 是三个选择中最强大的一种

    • 考虑play_again3的例子。当用户输入Ctrl-C,当前运行的程序会立即退出而不调用恢复驱动程序设置的函数。更好的做法是,程序在接收到SIGINT后,调用一个恢复设置的函数,然后再退出。

    • 通过 signal(signum, functionname);可以在信号到来时调用信号处理函数

      1
      2
      3
      4
      5
      #include <signal.h>

      typedef void (*sighandler_t)(int);

      sighandler_t signal(int signum, sighandler_t handler);
    • signum为需相应的信号,handler为如何响应

    • handler可以是函数名或以下两个特殊值之一:

      • SIG_IGN,忽略信号
      • SIG_DFL,将信号恢复为默认处理
    • signal返回前一个处理函数。值是指向函数的指针

信号处理的例子

捕捉信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <signal.h>

main(){
void f(int);
int i;
signal(SIGINT,f);
for(i=0;i<5;++i){
printf("hello%d\n",i);
sleep(1);
}
}

void f(int signum){
printf("OUCH! \n");
}

编译并运行,在中途击键Ctrl-C

1
2
3
4
5
6
7
$ ./sigdemo1 
hello0
hello1
hello2
^COUCH!
hello3
hello4

忽略信号

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <signal.h>

main(){
void f(int);
int i;
signal(SIGINT,SIG_IGN);
printf("you can't stop me!");
for(i=0;i<5;++i){
printf("haha%d\n",i);
sleep(1);
}
}

编译并运行,随意击键Ctrl-C不会有影响

1
2
3
4
5
6
$ ./sigdemo2
you can't stop me!haha0
haha1
^Chaha2
^Chaha3
haha4

为处理信号做准备:play_again4.c

下面这个程序版本捕捉SIGINT,重置驱动程序,然后返回no的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// play_again4.c
// purpose: ask if user wants another transaction
// method: set tty into char-by-char mode, no -echo mdoe
// set tty into no -delay mode
// read char, return result
// resets terminal modes on SIGINT, ignores SIGQUIT
// returns: 0=>yes, 1=>no, 2=>timeout
// better: reset terminal mode on Interrupt

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

#define QUESTION "Do you want another transaction"
#define TRIES 3
#define SLEEPTIME 2
#define BEEP putchar('\a')

extern void tty_mode(int how);
extern void set_nodelay_mode(void);
extern void set_cr_noecho_mode(void);
extern void ctrl_c_handler(int);
extern int get_ok_char(void);
extern int get_response(char *);

int maxtries = TRIES; // 教材版源代码实际上漏掉了该变量的声明

int main(int argc, char *argv[]) {
tty_mode(0); // save tty mode
set_nodelay_mode();
set_cr_noecho_mode();
signal(SIGINT, ctrl_c_handler);
signal(SIGQUIT, SIG_IGN);
int response = get_response(QUESTION);
tty_mode(1); // reload tty mode
return response;
}

extern void tty_mode(int how) {
static struct termios original_mode;
if (how == 0) {
tcgetattr(0, &original_mode);
}
else {
tcsetattr(0, TCSANOW, &original_mode);
}
}

extern void set_nodelay_mode(void) {
int termflags;
termflags = fcntl(0, F_GETFL);
termflags |= O_NDELAY;
fcntl(0, F_SETFL, termflags);
}

extern void set_cr_noecho_mode(void) {
struct termios ttystate;
tcgetattr(0, &ttystate);
ttystate.c_lflag &= ~ICANON; // no buffering
ttystate.c_lflag &= ~ECHO; // no echo either
ttystate.c_cc[VMIN] = 1; // get one char at a time
tcsetattr(0, TCSANOW, &ttystate);
}

extern void ctrl_c_handler(int signum) {
tty_mode(1);
exit(EXIT_FAILURE);
}

extern int get_ok_char(void) {
int c;
while ((c = getchar()) != EOF && strchr("yYnN", c) == NULL) {
;
}
return c;
}

extern int get_response(char *question) {
int c;
printf("%s (y/n)?", question);
fflush(stdout);
while (true) {
sleep(SLEEPTIME);
c = tolower(get_ok_char());
if (c != EOF)
putchar(c);
if (c == 'y')
return 0;
if (c == 'n')
return 1;
if (maxtries-- == 0)
return 2;
BEEP;
}
}

事件驱动编程:编写一个视频游戏

视频游戏做什么:

考虑一个有两个人参与的星际旅行视频游戏:

  • 程序创立行星、流星、飞船和其他物体的影像,并使它们移动。
  • 每一个物体有自己的移动速度、方向、动力和其他一些属性。
  • 物体之间相互作用。一个流星可能撞上飞船或其他流星。

游戏同时要相应用户输入:

  • 游戏玩家们通过按钮、鼠标和轨迹球在任何时刻都有可能生成输入,程序必须在很短的时间里作出相应。
  • 这些输入事件会影响游戏中物体的属性。通过按下按钮,用户可以增加飞船速度或是减小飞船质量。
  • 飞船的变化会影响它与其他物体的作用方式

如何做

一个视频游戏综合了一些基本的概念和原则:

  • 空间
    • 游戏必须在计算机屏幕的特定位置画影像。(程序如何控制视频显示?)
  • 时间
    • 影像以不同的速度在屏幕上移动。以一个特定的时间间隔改变位置。(程序是如何获知时间并且在特定的时间安排事情的发生?)
  • 中断
    • 程序在屏幕上平滑地移动物体,用户可在任意时刻产生输入。(程序是如何响应中断的?)
  • 同时做几件事
    • 游戏必须在保持几个物体移动的同时还要响应中断。

操作系统面临类似的问题

操作系统也要面对以上四个问题:

  • 内核将程序载入内存空间并维护每个程序在内存中所处的位置
  • 在内核的调度下,程序以时间片间隔的方式运行,同时,内核也在特定的时刻运行特定的内部人物。
  • 内核必须在很短的时间内响应用户和外设在任何时刻的输入。
  • 内核在同时做几件事的同时还需要保证数据的有序和规整

本章将通过编写一个基于字符终端的动画游戏来学习屏幕管理、时间、信号、和共享资源是如何安全地同时做几件事情的。

任务:单人弹球游戏(Pong)

概要描述:

  • 球以一定的速度移动
  • 求碰到墙壁或挡板会被弹回
  • 用户按按钮来控制挡板上下移动

屏幕编程:curses库

curses库是一组函数,程序员可以用它们来设置光标的位置和终端屏幕上显示的字符样式。

curses将终端屏幕看成是由字符单元组成的网格,每一个单元由(行、列)坐标对标示。坐标系的原点是屏幕的左上角,行坐标自上而下递增,列坐标自左向右递增。

curses具有的函数包括可以将光标移动到屏幕上任何行、列单元,添加字符到屏幕或者从屏幕上删除字符,设置字符的可视属性(如颜色、亮度),建立和控制窗口以及其他文本区域。我们会用到其中的9个

小deno展示curses程序基本逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <curses.h>

int main(void) {
initscr();

clear();
move(10, 20);
addstr("Hello, world");
move(LINES - 1, 0);
refresh();
getch(); //等待用户输入
endwin();
return EXIT_SUCCESS;
}

编译和运行:(-l指定连接库的名字)

1
2
[zhangqi@localhost demo]$ cc hello1.c -l curses -o hello1
[zhangqi@localhost demo]$ ./hello1

例2:hello2.c

将curses函数与循环、变量和其他函数组合在一起会产生更复杂的显示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <curses.h>

main(){
int i;

initscr();
clear();
for(i=0;i<LINES;++i){
move(i,i+1);
if(i%2 == 1) standout();
addstr("Hello, world");
if(i%2==1) standend();
}
refresh();
getch();
endwin();
}

运行结果:

curses内部:虚拟和实际屏幕

我们可以发现注释掉refresh函数会导致屏幕上什么也不显示。

curses设计成能够在不阻塞通信线路的情况下更新文本屏幕。surse通过虚拟屏幕来最小化数据流量

  • 真实屏幕是眼前的一个字符数组。
  • curses保留了屏幕的两个内部版本。一个内部屏幕是真实屏幕的复制。另一个是工作屏幕,其上记录了对屏幕的改动。
  • 每个函数,如move、addstr等都只在工作屏幕上进行修改。
  • 工作屏幕就像磁盘缓存,curses中的大部分的函数都只对它进行修改

refresh函数比较工作屏幕和真实屏幕的差异。然后refresh通过终端驱动送出那些能使真实屏幕与工作屏幕一直的字符和控制码。(替换有差异的部分)

时钟编程:sleep

为了写一个视频游戏,需要把影像在特定的时间置于特定的位置。用curses把影像置于特定的位置。然后在程序中添加时间响应。

第一步使用系统函数sleep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <curses.h>

main(){
int i;

initscr();
clear();
for(i=0;i<LINES;++i){
move(i,i+1);
if(i%2 == 1) standout();
addstr("Hello, world");
if(i%2==1) standend();

sleep(1);
refresh();
}
endwin();
}

当编译并运行这个程序的时候,可以看到hello字符串在屏幕自上而下逐行显示,每秒增加一行,反色和正常显示交替出现。注意必须每次循环调用refresh

动画例子2:hello4.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <curses.h>

main(){
int i;

initscr();
clear();
for(i=0;i<LINES;++i){
move(i,i+1);
if(i%2 == 1) standout();
addstr("Hello, world");
if(i%2==1) standend();

refresh();
sleep(1);
move(i,i+1);
addstr(" ");//erase line
}
endwin();
}

hello4创造移动的假象。字符串沿着对角线缓慢向下移动。方法是先在一个地方画字符串,睡眠一秒钟,然后在原来的地方画空字符串以删除原有影像,最后将输出位置推进。

注意在两次请求之后通过调用refresh来保证每次循环后旧的影像消失,新的影像显示

hello5将字符串在屏幕左右壁弹来弹去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <curses.h> // initscr(), clear(), standout(), standend(), refresh(),
// addstr(), move(), endwin()
#include <stdbool.h> // true
#include <unistd.h> // sleep()

#define LEFTEDGE 10
#define RIGHTEDGE 30
#define ROW 10

int main(void) {
char message[] = "hello";
char blank[] = " ";

initscr();
clear();
int dir = 1;
int pos = LEFTEDGE;
while (true) {
move(ROW, pos);
addstr(message);
move(LINES - 1, COLS - 1);
refresh();
sleep(1);
move(ROW, pos);
addstr(blank);
pos += dir;
if (pos >= RIGHTEDGE)
dir = -1;
if (pos <= LEFTEDGE)
dir = 1;
}
return EXIT_SUCCESS;
}

目前距离写出一个简单的动作类游戏:

  • 一秒钟时延太长,需要更精确的计时器
  • 需要增加用户输入

从而引出新话题:时钟和高级信号编程

时钟编程1:Alarms

程序可以以不同的方式使用时钟。可以用来在执行流中加入时延。

sleep(n)将当前进程挂起n秒或者在此期间被一个不能忽略的信号的到达所唤醒

sleep()是如何工作的:使用Unix中的Alarms

系统中的每个进程都有一个私有的闹钟(alarm clock)。这个闹钟很像一个计时器,可以设置在一定秒数后闹铃。

时间一到,时钟就发送一个信号SIGALRM到进程。

除非进程为SIGALRM设置了处理函数(handler),否则信号将杀死这个进程。

sleep函数由三个步骤组成:

  1. 为SIGALRM设置一个处理函数
  2. 调用alarm(num_seconds);
  3. 调用pause
image-20220911070235187

系统调用pause挂起进程直到信号到达。任何信号都可以唤醒进程,而非仅仅等待SIGALRM。

因此可以写出sleep1.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<signal.h>

main(){
void wakeup(int);

//调用signal设置SIGALRM处理函数,然后调用alarm设置一个4秒的计时器,最后调用pause等待
printf("about to sleep for 4 seconds\n");
signal(SIGALRM,wakeup);
alarm(4);
pause();
printf("Morning so soon? \n");
}

void wakeup(int signum){
#ifdef SHHHH
printf("Alarm received from kernel\n");
#endif
}

这里调用pause的目的是挂起进程直到有一个信号被处理。当计时器计时4秒钟以后,内核送出SIGALRM给进程,导致控制从pause跳转到信号处理函数。

在信号处理程序中的代码被执行,然后控制返回。

当信号被处理完后,pause返回,进程继续

alarm的细节:

1
2
3
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
  • 入参:seconds 等待的时间(秒)
  • 如果出错,返回-1;否则返回计时器剩余时间

alarm设置本进程的计时器到seconds秒后激发信号。当设定的时间过去之后,内核发送SIGALRM到这个进程。如果计时器已经被设置,alarm返回剩余秒数

注意:调用alarm(0)意味着关掉闹钟

pause的细节

1
2
3
#include <unistd.h>

int pause(void);

pause挂起调用进程直到一个信号到达

  • 如果调用进程被这个信号终止,pause没有返回。
  • 如果调用进程用一个处理函数捕获,在控制从处理函数处返回后pause返回。这种情况下errno被设置为EINTR

计时器的另一个用途是调度一个在将来的某个时刻发生的动作同时做些其他事情:通过调用alarm来设置计时器,然后继续做别的事情。当计时器计时到0,信号发送,处理函数被调用。

时钟编程2:间隔计时器

sleep和alarm提供的时钟精度为秒,对很多应用来说精度不能让人满意。

后来添加进一个更强大的计时器系统:间隔计时器(interval timer),有更高的精度。并且每个进程都有3个独立的计时器而不是原来的一个。

每个计时器都有两个设置:初始间隔(it_value,要把两个都关掉,设为0)和重复间隔(it_interval,如果不想要重复,则设置为0)设置。

使用usleep(n)可以添加精度更高的时延,将当前进程挂起n微秒或者知道有一个不能被忽略的信号到达

三种计时器:真实、进程和实用

进程可以以3种方式来计时。考虑一个程序在运行了30s后结束。在一个分时系统中,这个程序不是一直在运行得,其他的程序与它共享处理器,下图显示了一种可能性:

显示了从0到5s进程在用户模式运行,从5s到15s睡眠,然后在核心态运行到20s睡眠……

因此从开始到结束,程序使用了10s用户时间、5s系统时间,并显示了3种时间:真实时间、用户时间和用户时间+系统时间

3类计时器名字和功能如下:

  • ITIMER_REAL
    • 计量真实时间。当这个计时器用尽,发送SIGALRM消息
  • ITIMER_VIRTUAL
    • 计量进程在用户态运行的时间。当虚拟计时器用尽,发送SIGVTALRM消息
  • ITIMER_PROF
    • 该计时器在进程运行于用户态或由该进程调用而陷入核心态时计时。当这个计时器用尽,发送SIGPROF消息

用间隔计时器编程

  • 选择计时器类型
  • 选择初始间隔和重复间隔
  • 设置在结构体struct itimerval中的值
  • 通过getitimer读取计时器设置

间隔计时器例子:ticker_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h> // setitimer()
#include <signal.h> // signal()
#include <unistd.h> // pause()
#include <stdbool.h>

extern void countdown(int);
extern int set_ticker(int n_msecs);

int main(void) {
//使用signal设置函数countdown来处理SIGALRM信号
signal(SIGALRM, countdown);

//通过set_ticker来设置微秒数
if (set_ticker(500) == -1) {
perror("set_ticker");
exit(EXIT_FAILURE);
}
else {
while (true)
pause();
}
return EXIT_SUCCESS;
}

extern void countdown(int signum) {
static int num = 10;
printf("%d..\n", num);
num--;
if (num < 0) {
printf("Done!\n");
exit(EXIT_SUCCESS);
}
}

extern int set_ticker(int n_msecs) {
//通过装载初始间隔和重复间隔设置间隔计时器
//每个间隔由秒数和微秒数组成
struct itimerval new_timeset;
long n_sec, n_usecs;

n_sec = n_msecs / 1000;
n_usecs = (n_msecs % 1000) * 1000L;

//设置初始间隔
new_timeset.it_value.tv_sec = n_sec;
new_timeset.it_value.tv_usec = n_usecs;

//设置重复间隔
new_timeset.it_interval.tv_sec = n_sec;
new_timeset.it_interval.tv_usec = n_usecs;

return setitimer(ITIMER_REAL, &new_timeset, NULL);
}

相关系统调用:

1
2
3
4
5
#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);

参数:

  • which:获取或设置的计时器
  • val:指向当前设置值的指针
  • newval:指向要被设置值的指针
  • oldval:指向要被替换的设置值的指针

返回值:

  • -1 出错
  • 0 成功

计算机有几个时钟

有些系统同时又几百个进程在运行。但计算机里没有几百个独立的时钟,一个系统只需要一个时钟来设置节拍。

一个硬件时钟的脉冲是计算机里唯一需要的时钟。

如何只用一个时钟在设置一个进程的私有计时器为5s的同时又设置另一个进程的私有计时器为12s?

  • 每个进程设置自己的计数时间,操作系统在每过一个时间片后为所有的计数器的数值做递减

信号处理1:使用signal

要实现视频游戏还需要管理中断的技术

中断处理是操作系统和系统软件的关键部分。Unix中的软件终端被称为信号(signals)。

早期的信号处理机制

各种事件(包括用户的击键、进程的非法操作和计时器到时)促使内核向进程发送信号。

早期信号处理模型,一个进程调用signal在以下三种处理信号的方法之中选择:

  • 默认操作(一般是终止进程),比如,signal(SIGALRM, SIG_DFL)
  • 忽略信号,比如,signal(SIGALRM, SIG_IGN)
  • 调用一个函数,比如,signal(SIGALRM, handler)

处理多个信号

如果只有一个信号要处理,原始的信号处理模型足以应付,但如果有多个信号到达会发生什么?

如果相应定义为终止(termination)或是忽略(ignore),那结果很清楚,但是如果是调用(invoke)一个函数来相应,那么结果就不那么明显了

捕鼠器问题

信号处理函数有点像捕鼠器。一个信号意味着什么具有破坏性的事情发生,并被捕获。当信号或老师被捕获,信号处理函数或捕鼠器就失效了。

在早期的版本中,信号处理函数在另一个方面也很像捕鼠器:在每次捕获之后,都必须重新设置它们。

如:

1
2
3
4
void handler(int s){
signal(SIGINT, handler);//reset handler
... //do work here
}

就算设置的速度非常快,它还是需要时间的,这一个间隙使得原有的信号处理不可靠。

进程的多个信号

想象一个进程在内存里工作,用户可能通过按下Ctrl-C来产生一个SIGINT信号,或者是Ctrl-\产生SIGQUIT信号,或者计时器到时产生一个SIGQLRM信号。

这些信号可能同时到达,那么进程要如何响应?

测试多个信号

编译并运行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

#define INPUTLEN 100

extern void int_handler(int );
extern void quit_handler(int );

int main(void) {
signal(SIGINT, int_handler);
signal(SIGQUIT, quit_handler);

int nchars;
char input[INPUTLEN];
do {
printf("\nType a message\n");
nchars = read(0, input, (INPUTLEN - 1));
if (nchars == -1)
perror("read returned an error");
else {
input[nchars] = '\0';
printf("You typed: %s\n", input);
}
} while (strncmp(input, "quit", 4) != 0);

return EXIT_SUCCESS;
}

extern void int_handler(int signum) {
printf("Received signal %d .. wating\n", signum);
sleep(2);
printf("Leaving int_handler\n");
}

extern void quit_handler(int signum) {
printf("Received signal %d .. waiting\n", signum);
sleep(3);
printf("Leaving quit_handler\n");
}

用不同的方式常规输入和两个信号生成键Ctrl-C和Ctrl-\ (分别对应SIGINT和SIGQUIT)。

  • SIGY打断SIGX的处理函数:当接连按下Ctrl-C和Ctrl-\时,会看到程序先跳到int_handler,接着调到quit_handler,然后再回到int_handler,最后回到主循环
1
2
3
4
^CReceived signal 2 .. waiting
^\Received signal 3 .. waiting
Leaving quithandler
Leaving inthandler
  • SIGX打断SIGX的处理函数:我这的实验结果是会阻塞第二个信号直到第一个处理完毕
  • 被中断的系统调用:例如程序经常在等待输入的时候接收到信号

信号机制其他的弱点

早期信号系统还有两个弱点:

  1. 不知道信号被发送的原因

    • 信号处理函数是一个在信号到达的时候被调用的函数。内核传给处理函数一个信号数字编号。因此早期的模型只告诉了处理函数它被调用是由什么类型的信号而引起的,但是没有告知为什么会生成信号。
    • 例如几种算术错误(除数为0,整数溢出和浮点下溢)会引起浮点异常(floating - point exception)。处理函数需要指导问题的原因
  2. 处理函数中不能安全地阻塞其他消息

    • 假设想让程序在相应SIGINT时忽略SIGQUIT,使用经典信号机制修改int_handler:

      1
      2
      3
      4
      5
      6
      7
      void int_handler(int s){
      int rv;
      void(*prev_qhandler)();
      prev_qhandler = signal(SIGQUIT, SIG_IGN);
      //......
      signal(SIGQUIT, prev_qhandler);
      }
    • 这样,在进入中断处理函数时禁用退出处理函数,在结束时再重新使能它。

    • 这个方案有两个问题:

      • 在调用int_handler和调用signal之间是它的软肋所在。这里只是希望调用int_handler和忽略SIGQUIT同时进行
      • 这里并不想忽略SIGQUIT,而只是想阻塞它直到int_handler处理完成。

信号处理2:sigaction

针对原有模型产生的问题,有不同的解决方案,这里介绍POSIX模型和相关的系统调用。

注意经典的信号系统依旧被支持,而且在一些应用中这些就够了

处理多个信号:sigaction

在POSIX中用sigaction替代signal。参数非常相似。指定什么信号将被如何处理。

1
2
3
4
#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

参数:

  • signum指明想要处理的消息
  • act 指向描述如何响应信号的结构体
  • oldact 如果不是null的话,就是指向描述被替换的处理设置的结构体

返回:

  • 如果新的操作设置成功则返回0
  • 设置错误返回-1

定制信号处理:struct sigaction

过去面对信号的处理只有三种选择:SIG_DFL、SIG_IGN、函数处理

这些选项在新的系统中作为结构体sigaction的部分定义依然提供。

结构体sigaction定义了如何处理一个信号,结构体完整定义:

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

sa_handler:信号处理器函数的地址,亦或是常量SIG_IGNSIG_DFL之一。仅当sa_handler是信号处理程序的地址时,亦即sa_handler的取值在SIG_IGNSIG_DFL之外,才会对sa_masksa_flags字段加以处理。

sa_sigaction:如果设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。

sa_mask:定义一组信号,在调用由sa_handler所定义的处理器程序时将阻塞该组信号,不允许它们中断此处理器程序的执行。

sa_flags:位掩码,指定用于控制信号处理过程的各种选项。

  • SA_NODEFER:捕获该信号时,不会在执行处理器程序时将该信号自动添加到进程掩码中。
  • SA_ONSTACK:针对此信号调用处理器函数时,使用了由sigaltstack()安装的备选栈。
  • SA_RESETHAND:当捕获该信号时,会在调用处理器函数之前将信号处置重置为默认值(即SIG_IGN)。
  • SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息

选择sa_handler还是sa_sigaction?

  • 如果老的处理方式就够用了,可以设置sa_handler为其中之一。当然,如果指定为旧的信号处理方式,那么只能得到信号编号
  • 如果设定sa_sigaction为一个处理函数,那么哪个处理函数被调用的时候,不但可以得到信号编号而且可以获悉被调用的原因以及产生问题的上下文的相关信息。
  • 为了告诉内核使用的是新的信号处理方式,只需设置sa_flags的SA_SIGINFO位

sa_flags

用一些位来控制处理函数,可以解决前面提出的问题:

sa_mask

1
sa_mask specifies a mask of signals  which  should  be  blocked  (i.e., added  to  the signal mask of the thread in which the signal handler is invoked) during execution of the signal handler.  In addition, the signal  which triggered the handler will be blocked, unless the SA_NODEFER flag is used.

sa_mask决定在处理一个消息时是否要阻塞其他信号。

sa_mask中的位指定哪些信号要被阻塞。

sa_mask的值包括要被阻塞的信号集

例子:使用sigaction

演示了如何使用sigaction,注意程序做到了在处理SIGINT时阻塞SIGQUIT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>

#define INPUTLEN 100

extern void sig_handler(int );

int main(void) {
struct sigaction newhandler;
sigset_t block;
char input[INPUTLEN];
newhandler.sa_handler = sig_handler;
newhandler.sa_flags = SA_RESETHAND | SA_RESTART;

sigemptyset(&block);
sigaddset(&block, SIGQUIT);
newhandler.sa_mask = block;

if (sigaction(SIGINT, &newhandler, NULL) == -1) {
perror("sigaction");
}
else {
while (true) {
fgets(input, INPUTLEN, stdin);//从输入流中读取INPUTLEN长度的字符存到input中
printf("input: %s\n", input);
}
}

return EXIT_SUCCESS;
}

extern void sig_handler(int signum) {
printf("Called with signal %d\n", signum);
sleep(signum);
printf("done handling signal %d\n", signum);
}

运行这个程序,如果以很快的速度连续按Ctrl-C和Ctrl-\,退出信号将被阻塞直到中断信号处理完毕。

如果连续按两下Ctrl-C,进程就将被第二个信号杀死

如果想要捕获所有的Ctrl-C,将SA_RESETHAND掩码从sa_flags中去掉

防止数据损毁(Data Corruption)

程序在由于中断转到别的地方执行的情况可能会导致数据损毁

因此在一些情况下一个操作不应该被其他操作打断。在对一个数据结构(这里是列表)改动结束之前,其它函数不能读或写这个数据结构。

临界区(Critical Sections)

一段修改一个数据结构的代码如果在运行时被打断将导致数据的不完整或损毁,则称这段代码为临界区。

当程序处理信号时,必须决定哪一段代码为临界区,然后设法保护这段代码。临界区不一定就在信号处理函数中,很多出现在常规的程序流中。

保护临界区的最简单的办法就是阻塞或忽略那些处理函数将要使用或修改特定数据的信号。

阻塞信号:sigprocmask和sigsetops

  1. 在信号处理者一级阻塞信号

    为了在处理一个信号的时候阻塞另一个信号,要设置struct sigaction结构中的sa_mask成员位,它在设置处理函数时被传递给sigaction。sa_mask是sigset_t类型,它定义了一个信号集。

  2. 在进程一级阻塞信号

    在任何时候一个进程都有一些信号被阻塞(注意是阻塞而不是忽略)。这个信号集就称为signal mask。通过sigprocmask可以修改这个被阻塞的信号集,sigprocmask作为一个原子操作根据所给的信号集来修改当前被阻塞的信号集

    1
    2
    3
    4
    5
    6
    7
    8
    #include <signal.h>

    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    /* sigprocmask 修改当前的signal mask设置。
    how的值分别为SIG_BLOCK、SIG_UNBLOCK或SIG_SET时,
    *set所指定的信号将被添加、删除或替换
    如果oldset不为NULL,那么之前的设置将被复制到oldset中
    */
  3. 用sigsetops构造信号集

    一个sigset_t是一个抽象的信号集,可以通过一些函数来添加或删除信号。基本的函数如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <signal.h>

    //清除由set指向的列表中的所有信号
    int sigemptyset(sigset_t *set);

    //添加所有的信号到set指向的列表
    int sigfillset(sigset_t *set);

    //添加signum到set指向的列表
    int sigaddset(sigset_t *set, int signum);

    //从set指向的列表中删除signum所标识的信号
    int sigdelset(sigset_t *set, int signum);

    int sigismember(const sigset_t *set, int signum);

例子:暂时阻塞用户信号

1
2
3
4
5
6
7
sigset_t sigs, prevsigs;
sigemptyset(&sigs); //turn off all bits
sigaddset(&sigs, SIGINT); //turn on SIGINT bit
sigaddset(&sigs, SIGQUIT); //turn on SIGQUIT bit
sigprocmask(SIG_BLOCK, &sigs, &prevsigs); //add that to proc mask
//..modify data structure here
sigprocmask(SIG_SET, *prevsigs, NULL); //restore previous mask

重入代码(Reetrant Code):递归调用的风险

一个信号处理者或者一个函数,如果在激活状态下能被调用而不引起任何问题就称之为可重入的。

在通过sigaction设置时,可以通过设置SA_NODEFER位来允许处理函数的递归调用。反之,可以通过清除此位来阻塞信号。如何选择:

  • 如果处理者是不可重入的,必须阻塞信号。但是如果阻塞信号,就有可能丢失信号

如何权衡丢掉信号还是弄乱数据是需要认真考虑的

kill:从另一个进程发送的信号

信号来自间隔计时器、终端驱动、内核或者进程。一个进程可以通过kill系统调用向另一个进程发送信号

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

入参:

  • pid 目标进程id
  • sig要被发送的信号

返回值:

  • -1 失败
  • 0 成功

kill向一个进程发送一个信号。发送信号的进程的用户ID必须和目标进程的用户ID相同,或者发送信号的进程的拥有者是一个超级用户。一个进程可以向自己发送信号。

一个进程可以向其他进程发送任何信息,包括一般来自键盘、间隔计时器或者内核的信号。

比如一个进程可以向另一个进程发送SIGSEGV信号,就好像目标进程执行了非法内存读取

Unix命令kill使用kill系统调用

进程间通信的含义

接受信号的进程几乎可以设置任何信号的处理者。

考虑在收到SIGINT时就打印OUCH!的程序。如果其他进程向OUCH!程序发送SIGINT又该如何呢?——OUCH!程序会捕获信号,跳转到处理者,打印OUCH!

更进一步。如果第一个程序设置一个间隔计时器,计时器的信号处理函数向OUCH!程序发送SIGINT信号。这样相应的处理函数就被调用。从而一个进程的计时器控制了另一个进程的函数调用。

IPC信号设计:SIGUSR1、SIGUSR2

Unix有两个信号可以被用户程序使用。它们是SIGUSR1和SIGUSR2。这两个信号没有预定义任务。可以使用它们避免使用已经有预定义语义的信号。

编程时有很多方法组合使用kill和sigaction

视频游戏中的临界区

球匀速在屏幕上移动,碰到墙或挡板就弹回。用户通过按键上下移动挡板。间隔计数器控制球的运动。用户控制挡板的输入就如信号那样看上去是无法预料的事件。

因此要考虑:需要在某个时刻阻塞用户输入吗?有没有什么临界区,其间挡板不应该移动吗?

使用计时器和信号:视频游戏

回到视频游戏。游戏有两个主要元素:动画和用户输入。动画要平滑,用户输入会改变运动状态。

先设计一个可以让用户可以将字符串在屏幕上弹来弹去的程序:

程序将一个单词平滑地在屏幕上移动:

  • 当用户按下空格键,单词就向反方向移动
  • s键和f键分别增加和减少单词的移送速度
  • 按“Q”键退出程序

动画的实现是在一个地方画一个字符串,等待几毫秒,然后擦去旧的影像并在原来位置的左边或右边一个单位距离重新画同一个字符串

  • 这里希望擦去和重画动作以相同的间隔连续的进行,因此使用间隔计时器来调用相应的处理函数
  • 两个变量分别记录移动的方向和速度:设置方向变量的值+1和-1分别表示向左和向右移动。
  • 延时变量记录间隔计时器的间隔长度:较长的延时意味着较慢的速度
  • 向程序添加方向和速度控制:根据用户的键盘输入修改方向和速度变量

如下图,体现了程序的逻辑

该设计体现了两个重要的技术:状态变量和事件处理:

  • 记录位置、方向和延时的变量定义了动画的状态
  • 用户输入和计时器信号是改变这些状态的事件
  • 每次计时器到达信号就调用改变位置的处理函数
  • 每次得到用户键盘输入信号就调用改变方向和速度变量的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// crmode() 已改爲 cbreak(), 此外別忘了自己定義 set_ticker
// 此外 row, col, direction, delay, new_delay, c 等变量要声明为全局变量

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <curses.h>
#include <stdbool.h>
#include <string.h>

#define MESSAGE "hello"
#define BLANK " "

int row = 10, col = 0;
int direction = 1;
int delay = 200, new_delay = 0;
int c = 0;

extern int set_ticker(int );
extern void move_msg(int );

int main(void) {
initscr();
cbreak();
noecho();

move(row, col);
addstr(MESSAGE);
signal(SIGALRM, move_msg); //当收到时钟信号时,执行move_msg
set_ticker(delay); //设置间隔计时器的初始状态

while (true) {
new_delay = 0;
c = getch();
if (c == 'Q') break;
if (c == ' ') direction *= -1;
if (c == 'f' && delay > 2)
new_delay = delay / 2;
if (c == 's')
new_delay = delay * 2;
if (new_delay > 0)
set_ticker(delay = new_delay);
}

endwin();
return EXIT_SUCCESS;
}

extern int set_ticker(int n_msecs) {
struct itimerval new_timeset;
long n_sec, n_usecs;

n_sec = n_msecs / 1000;
n_usecs = (n_msecs % 1000) * 1000L;

new_timeset.it_value.tv_sec = n_sec;
new_timeset.it_value.tv_usec = n_usecs;

new_timeset.it_interval.tv_sec = n_sec;
new_timeset.it_interval.tv_usec = n_usecs;

return setitimer(ITIMER_REAL, &new_timeset, NULL);
}

extern void move_msg(int signum) {
signal(SIGALRM, move_msg);

move(row, col);
addstr(BLANK);
col += direction;
move(row, col);
addstr(MESSAGE);
refresh();

if (direction == -1 && col <= 0)
direction = 1;
if (direction == 1 && col + strlen(MESSAGE) >= COLS)
direction = -1;
}

bounce2d.c:二维动画

改进,用户可以控制水平速度和垂直速度
为了能同时在两个方向移动,要用两个计数器来充当计时器

bounce.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// some settings for the game

#define BLANK ' '
#define BALL_SYMBOL 'O'

#define TOP_EDGE 5
#define BOTTOM_EDGE 20
#define LEFT_EDGE 10
#define RIGHT_EDGE 70

#define TICKS_PER_SEC 50

#define X_INIT_POS 10
#define Y_INIT_POS 10
#define X_TIM 5
#define Y_TIM 8

struct pinball {
int x_pos, y_pos,
x_ttm, y_ttm,
x_ttg, y_ttg,
x_dir, y_dir;
char symbol;
};

bounce2d.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <stdbool.h>
#include <curses.h>
#include "bounce.h"

struct pinball the_ball;

extern void set_up();
extern void ball_move(int );
extern void bounce_or_lose(struct pinball *);
extern void end_down();
extern void set_ticker(int );

int main(void) {
set_up();//初始设置

int c;
while ((c = getch()) != 'Q') {
if (c == 'f') the_ball.x_ttm--;
if (c == 's') the_ball.x_ttm++;
if (c == 'F') the_ball.y_ttm--;
if (c == 'S') the_ball.y_ttm++;
}

end_down();
return EXIT_SUCCESS;
}

extern void set_up() {
//设置初始位置
the_ball.x_pos = X_INIT_POS;
the_ball.y_pos = Y_INIT_POS;
//设置x和y方向计数器的间隔和当前值(离下次重画还要多少个时钟信号)
the_ball.x_ttm = the_ball.x_ttg = X_TIM;
the_ball.y_ttm = the_ball.y_ttg = Y_TIM;
//设置初始方向
the_ball.x_dir = 1;
the_ball.y_dir = 1;
//设置移动的字符
the_ball.symbol = DFL_SYMBOL;

initscr();
noecho();//用户的输入不显示
cbreak();//不缓存输入,直接单个字符读入

signal(SIGALRM, SIG_IGN);//忽略计时器信号
mvaddch(the_ball.y_pos, the_ball.x_pos, the_ball.symbol);
refresh();//刷新屏幕 进行显示

signal(SIGALRM, ball_move);//重新启动处理计时器信号
set_ticker(1000 / TICKS_PER_SEC); //设置间隔计时器
}

extern void ball_move(int signum) {
signal(SIGALRM, SIG_IGN);
int x_org_pos = the_ball.x_pos,
y_org_pos = the_ball.y_pos;
bool moved = false;

if (the_ball.x_ttm > 0 && the_ball.x_ttg-- == 1) {
the_ball.x_pos += the_ball.x_dir;
the_ball.x_ttg = the_ball.x_ttm;
moved = true;
}
if (the_ball.y_ttm > 0 && the_ball.y_ttg-- == 1) {
the_ball.y_pos += the_ball.y_dir;
the_ball.y_ttg = the_ball.y_ttm;
moved = true;
}

if (moved == true) {
mvaddch(y_org_pos, x_org_pos, BLANK);
mvaddch(the_ball.y_pos, the_ball.x_pos, the_ball.symbol);
bounce_or_lose(&the_ball);
move(LINES - 1, COLS - 1);//光标移到右下角
refresh();
}

signal(SIGALRM, ball_move);
}

extern void bounce_or_lose(struct pinball *the_ball_p) {
if (the_ball_p->y_pos == TOP_EDGE)
the_ball_p->y_dir = 1;
if (the_ball_p->y_pos == BOTTOM_EDGE)
the_ball_p->y_dir = -1;
if (the_ball_p->x_pos == LEFT_EDGE)
the_ball_p->x_dir = 1;
if (the_ball_p->x_pos == RIGHT_EDGE)
the_ball_p->x_dir = -1;
}

extern void end_down() {
set_ticker(0);//关闭计时器,不再发送时钟信号
endwin();
}

extern void set_ticker(int n_msecs) {
struct itimerval new_timeset;

long n_sec = n_msecs / 1000;
long n_usecs = (n_msecs % 1000) * 1000L;

new_timeset.it_value.tv_sec = n_sec;
new_timeset.it_value.tv_usec = n_usecs;

new_timeset.it_interval.tv_sec = n_sec;
new_timeset.it_interval.tv_usec = n_usecs;

setitimer(ITIMER_REAL, &new_timeset, NULL);
}

输入信号:异步I/O

前面的程序通过调用getch()阻塞程序以等待键盘输入。

除了阻塞,程序还可以要求内核在得到输入时发送信号,这样就可以不用一直等待着。

Unix有两个异步输入(asynchronous input)系统:

  • 一种是当输入就绪时发送信号。UCB中通过设置文件描述符(file descriptor)的O_ASYNC来实现
  • 另一个是当输入被读入时发送信号。是POSIX标准,它调用aio_read

新版本的反弹程序如图所示,需要两种信号:SIGIO和SIGALRM,所以要建立两个处理函数:

  • SIGIO处理函数读入击键并根据读入的数据采取行动
  • SIGALRM处理函数驱动动画并检测碰撞。(为了简单起见去掉了速度控制)

方法1:使用O_ASYNC

使用O_ASYNC需要对原来的弹球程序做4处改动:

  • 要建立和设置在键盘输入时被调用的处理函数

  • 使用fcntl的F_SETOWN命令告诉内核发送输入通知信号给进程。其他进程可能也连接到键盘,这里不想让这些进程发送信号

    • 补充fcntl相关函数:

      1
      2
      3
      4
      #include <unistd.h>
      #include <fcntl.h>

      int fcntl(int fd, int cmd, ... /* arg */ );
  • 通过调用fcntl来设置文件描述符0中的O_ASYNC位来打开输入信号

  • 最后循环调用pause等待来自计时器或键盘的信号

当有一个键盘来的字符到达,内核向进程发送SIGIO信号。SIGIO的处理函数使用标准的curses函数getch来读入这个字符。当计时器间隔超时,内核发送以前已经处理的SIGALRM信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <stdio.h>
#include <stdlib.h>
#include <curses.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/time.h>

#define MESSAGE "hello"
#define BLANK " "

int row =10, col =0;
int dir = 1, delay =200;
int done = 0;

void on_alarm(int);
void on_input(int);
void enable_kbd_signals();
int set_ticker(int);

int main(){
sigset_t * set;
set = (sigset_t*)malloc(sizeof(set));
sigemptyset(set);
sigaddset(set,SIGIO);
sigprocmask(SIG_UNBLOCK,set,NULL);

initscr();
crmode();
noecho();
clear();

enable_kbd_signals();
signal(SIGIO,on_input);
enable_kbd_signals();
signal(SIGALRM,on_alarm);
set_ticker(delay);

move(row,col);
addstr(MESSAGE);

while(!done) pause();

endwin();
return 0;
}

void on_input(int signum){
char c = getch();

if(c == 'Q' || c ==EOF){
done = 1;
//the rest 3 lines not included will cause cannot quit
move(LINES-1,0);
endwin();
exit(0);
}
else if(c == ' ') dir = -dir; //change direction when the space is input
}

void on_alarm(int signum){
signal(SIGALRM, on_alarm); //reset, to ensure the setting is right
mvaddstr(row, col, BLANK);
col+=dir;
mvaddstr(row,col,MESSAGE);
refresh();

if(dir == -1 && col <= 0) dir = 1;
else if(dir == 1 && col+strlen(MESSAGE)>=COLS)
dir = -1;

}

void enable_kbd_signals(){
int fd_flags;
fcntl(0, F_SETOWN,getpid());
fd_flags = fcntl(0,F_GETFL);
fcntl(0,F_SETFL,(fd_flags|O_ASYNC));
}

int set_ticker(int n_msecs){
struct itimerval new_timeset;
long n_sec, n_usecs;

n_sec = n_msecs/1000;
n_usecs = (n_msecs % 1000) *1000L;

new_timeset.it_interval.tv_sec = n_sec;
new_timeset.it_interval.tv_usec = n_usecs;

new_timeset.it_value.tv_sec = n_sec;
new_timeset.it_value.tv_usec = n_usecs;

return setitimer(ITIMER_REAL, &new_timeset, NULL);
}

方法2:使用aio_read

相比值文件描述符的O_ASYNC位,使用aio_read更加灵活,当然也更加复杂一点

对原来的弹球程序做4处改动:

  1. 设置输入被读入时调用的处理函数on_inpuut
  2. 设置struct kbcbuf中的变量来指明等待什么类型的输入,当输入发生时产生什么信号
    • 在这个程序中,需要从文件描述符0中读入一个字符,当字符被读入时希望收到SIGIO信号。
    • 实际上能指定任何信号,甚至是SIGARLM或SIGINT
  3. 通过将以上定义的结构体传给aio_read来递交读入请求。和调用一般的read不同,aio_read不会阻塞进程。相反,aio_read会在完成时发送信号
  4. 最后,实现处理函数,函数通过调用aio_return来得到输入的字符。然后处理这个字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // pause()
#include <fcntl.h> // fcntl()
#include <signal.h> // signal()
#include <sys/time.h> // set_ticker()
#include <stdbool.h>
#include <string.h>
#include <aio.h>
#include "curses.h" // initscr(), cbreak(), getch(), move(), addstr(), mvaddch(), refresh(), endwin()

#define MESSAGE "hello"
#define BLANK " "

struct aiocb kbcbuf;

int row = 10;
int col = 0;
int dir = 1;
int delay = 200;
bool done = false;

extern void set_ticker(int n_msecs);
extern void on_input(int signum);
extern void setup_aio_buffer();
extern void on_alarm(int signum);

int main(void) {
initscr();
cbreak();
noecho();
refresh();

signal(SIGIO, on_input);
setup_aio_buffer();
aio_read(&kbcbuf);

signal(SIGALRM, on_alarm);
set_ticker(delay);

while (done == false)
pause();

endwin();
return EXIT_SUCCESS;
}

extern void set_ticker(int n_msecs) {
long n_secs = n_msecs / 1000L;
long n_usecs = (n_msecs % 1000L) * 1000L;

struct itimerval new_timeset;

new_timeset.it_value.tv_sec = n_secs;
new_timeset.it_value.tv_usec = n_usecs;

new_timeset.it_interval.tv_sec = n_secs;
new_timeset.it_interval.tv_usec = n_usecs;

setitimer(ITIMER_REAL, &new_timeset, NULL);
}

extern void on_input(int signum) {
int c;
char *cp = (char *)kbcbuf.aio_buf;

if (aio_error(&kbcbuf) != 0) {
perror("reading failed");
}
else {
if (aio_return(&kbcbuf) == 1) {
c = *cp;
if (c == 'Q' || c == EOF)
exit(EXIT_SUCCESS);
else if (c == ' ')
dir = -dir;
}
}

aio_read(&kbcbuf);
}

extern void setup_aio_buffer() {
static char input[1];

kbcbuf.aio_fildes = 0;
kbcbuf.aio_buf = input;
kbcbuf.aio_nbytes = 1;
kbcbuf.aio_offset = 0;

kbcbuf.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
kbcbuf.aio_sigevent.sigev_signo = SIGIO;
}

extern void on_alarm(int signum) {
signal(SIGALRM, on_alarm);
mvaddstr(row, col, BLANK);
col += dir;
mvaddstr(row, col, MESSAGE);
refresh();

if (dir == -1 && col <= 0)
dir = 1;
else if (dir == 1 && col + strlen(MESSAGE) >= COLS)
dir = -1;
}

编译

1
$ cc bounce_aio.c -lcurses -lrt  -o bounce_aio

弹球程序中需要异步读入吗

不。

用户输入阻塞程序,间隔计时器驱动球移动的模式工作的很好。要注意异步读入的优势在于程序不用被输入阻塞而可以做些其他什么。

但是操作系统需要异步输入:

  • 内核要运行程序而不能把时间浪费在等待用户输入上
  • 内核设置当键盘、串口或网卡得到输入时被调用的处理函数
  • 内核从一个运行中的程序跳转到处理函数,处理输入,再跳回运行中的程序。
  • 在临界区,内核阻塞信号

内核的异步输入是由硬件实现的,而进程的异步输入是由软件实现的。

内核的设备驱动代码从输入端口读入字符,然后将读入的字符通过终端驱动进行处理。如果驱动的文件描述符被设置为异步输入,内核向进程发送信号。当进程继续运行时,控制转移到进程内的信号处理函数。

进程和程序:编写命令解释器sh

Unix是如何运行程序的?

首先登录,然后shell打印提示符,输入命令并按回车键。程序立即就开始运行了。

当程序结束后,shell打印一个新的提示符。

程序是什么

一个程序是存储在文件中的机器指令序列。一般它是由编译器将源代码编译成二进制格式的代码。

运行一个程序意味着将这个机器指令序列载入内存然后让处理器(CPU)逐条执行这些指令。

在Unix术语中,一个可执行程序是一个机器指令及其数据的序列。一个进程是程序运行时的内存空间和设置。

数据和程序存储在磁盘文件中,程序在进程中运行。

通过命令ps学习进程

进程存在于用户空间。

用户空间是存放运行的程序和它们的数据的一部分内存空间。可以通过ps(process status,进程状态)命令来查看用户空间的内容。这个命令会列出当前的进程。

1
2
3
4
$ ps
PID TTY TIME CMD
3042 pts/0 00:00:04 bash
122547 pts/0 00:00:01 ps

这里有两个进程正在运行:bash(shell)和ps命令。

每个进程都:

  • 有一个可以唯一标识它的数字。称为进程ID(PID)。
  • 与一个终端相连,这里是dev/pts/0。
  • 有一个已运行时间。注意ps对已运行时间通缉并不是非常的精确

和ls一样,ps支持-a,-a选项列出所有进程,包括在其他终端由其他用户运行的程序。但是带选项-a的输出并不包括shell

ps也有一个-l选项来打印更多细节

1
2
3
4
[zhangqi@localhost chap8]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 3042 3033 0 80 0 - 29246 do_wai pts/0 00:00:04 bash
0 R 1000 123930 3042 0 80 0 - 38331 - pts/0 00:00:00 ps
  • 名为S的列表示各个进程的状态,为R说明ps对应的进程正在运行,其他说明处于睡眠状态
  • UID列指明用户ID
  • 每个进程都有一个进程ID(PID),同时也有一个父进程ID(PPID)
  • 标记为PRI和NI的列分别是进程的优先级和niceness级别。内核根据这些值来决定什么时候运行进程
    • 一个进程可以增加niceness级别(就像让位)
    • 超级用户可以减少他的niceness级别(就像插队)
  • SZ列表示进程的大小。这列的数据表示这个进程占用的内存大小。程序在运行的时候占用的内存数量可能会动态的改变。如果程序在运行时分配内存,那么它占用的内存就会增加
  • WCHAN列显示进程睡眠的原因

和文件管理类似:

  • 文件包含数据,进程包含可执行代码
  • 文件和进程都有一些属性
  • 内核建立和销毁文件,进程类似
  • 就像管理磁盘的多个文件,内核管理内存中的多个进程,为它们分配空间,并记录内存分配情况

系统进程

除了用户运行的进程外,其他一些是Unix系统用来完成系统任务的进程,可通过 ps -ax查看

系统进程中的很大一部分是没有终端与之相连的。它们在系统启动时启动,而不是由用户在命令行输入。

这些系统进程的功能包括内核缓冲、虚存页面、管理系统日志、调度批任务、让一般的用户登录(sshd、getty)等

内存和程序

Unix系统中的内存分为系统空间和用户空间。

进程存在于用户空间。

内存实际上就是一个字节序列,或是一个很大的数组

  • 如果机器有64MB的内存,那意味着这个数组有大约6700万个内存位置。
  • 其中的一些用来存放组成内核的机器指令和数据。
  • 还有一些存放组成进程的机器指令和数据
  • 一个进程不一定必须要占一段连续的内存。就像文件在磁盘上被分成小块,进程在内存也被分成小块。
  • 同样和文件有记录分配了的磁盘块的列表相似,进程也有保存分配到的内存页面(memory pages)的数据结构

建立一个进程有点像建立一个磁盘文件:内核要找到一些用来存放程序指令和数据的空闲内存也。内核还要建立数据结构来存放相应的内存分配情况和进程属性

shell:进程控制和程序控制的工具

shell是一个管理进程和运行程序的程序。

就像存在很多编程语言,Unix系统有很多种可用的shell,所有常用的shell都有三个主要功能:

  • 运行程序
    • 如grep、date、ls、echo和mail都是一些普通的程序,用C编写,并被编译成机器语言。shell将它们载入内存并运行它们。很多人把shell看成一个程序启动器(program launcher)
  • 管理输入和输出
    • shell不仅仅是运行程序。使用< 、>和 | 符号可以将输入、输出重定向。这样就可以告诉shell将进程的输入和输出连接到一个文件或是其他的进程
  • 可编程
    • shell同时也是带有变量和流程控制的编程语言。

shell是如何运行程序的

shell打印提示符,输入命令,shell就运行这个命令,然后shell再次打印提示符——如此循环。

这些现象可以反映一个shell的主循环:

  • 用户键入a.out;
  • shell建立一个新的进程来运行这个程序
  • shell将程序从磁盘载入
  • 程序在它的进程中运行直到结束

即:

1
2
3
4
while( !end_of_input)
get command
execute command
wait for command to finish

考虑在终端先后执行ls 和 ps命令,时间轴如下:

需要三个技术:

  • 如何建立进程 fork
  • 如何运行一个程序 execvp
  • 如何让父进程等待子进程结束 wait

问题1:一个程序如何运行另一个程序

答案:程序调用execvp

比如为了运行 ls -la,一个程序调用execvp(“ls”,arglist)。这里arglist是命令行的字符串数组。内核从磁盘将程序载入内存。命令参数ls和-la被传给程序,然后程序开始运行:

  • 程序调用execvp
  • 内核从磁盘将程序载入
  • 内核将arglist复制到进程
  • 内核调用main(argc,argv)

代码:

1
2
3
4
5
6
7
8
9
10
main(){
char * arglist[3];

arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0;
printf("*** About toexec ls -l\n");
execvp("ls",arglist);
printf("*** ls is done. bye\n");
}

编译并运行后:

1
2
3
4
5
[zhangqi@localhost chap8]$ ./exec1
*** About toexec ls -l
total 16
-rwxrwxr-x. 1 zhangqi zhangqi 8408 Sep 13 07:45 exec1
-rw-rw-r--. 1 zhangqi zhangqi 182 Sep 13 07:45 exec1.c

可以发现第二条打印的消息并没有出现。

一个程序在一个进程中运行——也就是一些内存和内核中相应的数据结构。这样,execvp将程序从磁盘载入进程以便它可以被运行。

核心:内核将新程序载入到当前进程,替代当前进程的代码和数据

exec系统调用(execvp是一组基于execve系统调用函数中的一个,统称为exec)从当前进程中把当前程序的机器指令清除,然后在空的进程中载入调用时指定的程序代码,最后运行这个新的程序。exec调整进程的内存分配使之适应新的程序对内存的要求。

execvp()总结如下:

  • 参数: file,要执行的文件名,argv,字符串数组
  • 如果出错,返回-1,如果执行成功,execvp没有返回值,当前程序从进程中清楚,新的程序在当前进程中运行。
1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

execvp载入又file指定的程序到当前进程,然后试图运行它。execvp将以NULL结尾的字符串列表传给程序。execvp在环境变量PATH所指定的路径中查找file文件。

带提示符的shell实现

psh1.c (带提示符的shell,prompting shell)

该程序要求每个字符串单独的输入,第一个是程序名,然后依次是程序参数,程序包括两步:

  • 一个字符串一个字符串地构造参数列表arglist,最后在数组末尾加上NULL
  • 将arglist[0]和arglist数组传给execvp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 教材版 psh1.c 有内存泄漏……

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define MAXARGS 20
#define ARGLEN 100

extern char *makestring(char argbuf[]);
extern void execute(char *arglist[]);

int main(void) {
char *arglist[MAXARGS + 1];
int numargs = 0;
char argbuf[ARGLEN];
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);
while (--numargs)
free(arglist[numargs]);
numargs = 0;
}
}
numargs++;
}
return EXIT_SUCCESS;
}

extern char *makestring(char argbuf[]) {
argbuf[strlen(argbuf) - 1] = '\0';
char *cp = (char *)malloc(strlen(argbuf) + 1);
if (cp == NULL) {
fprintf(stderr, "no memory\n");
exit(EXIT_FAILURE);
}
strcpy(cp, argbuf);
return cp;
}

extern void execute(char *arglist[]) {
execvp(arglist[0], arglist);
perror("execvp failed");
exit(EXIT_FAILURE);
}

编译并运行,结果:

1
2
3
4
5
6
7
8
9
10
[zhangqi@localhost chap8]$ ./psh1
Arg[0]?ls
Arg[1]?-l
Arg[2]?.
Arg[3]?
total 32
-rwxrwxr-x. 1 zhangqi zhangqi 8408 Sep 13 07:45 exec1
-rw-rw-r--. 1 zhangqi zhangqi 182 Sep 13 08:28 exec1.c
-rwxrwxr-x. 1 zhangqi zhangqi 8928 Sep 13 11:46 psh1
-rw-rw-r--. 1 zhangqi zhangqi 781 Sep 13 11:46 psh1.c

问题:这里在执行指定的程序结束之后就退出了,shell不能再次接受新的命令,为了运行新的命令,用户不得不再次运行shell

为了shell做到在运行程序的同时还能等待下一个命令,需要启动一个新进程

问题2:如何建立新的进程?

答案:一个进程调用fork来复制自己

1
2
3
#include <unistd.h>

pid_t fork(void);

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构
  • 复制原来的进程到新的进程
  • 向运行进程集添加新的进程
  • 将控制返回给两个进程

测试fork:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//forkdemo1.c
#include <stdio.h>

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);
}

编译并运行:(注意三行输出,可知新的进程3359是从fork返回的地方开始运行的,相当于分出一个岔路)

1
2
3
4
[zhangqi@localhost chap8]$ ./forkdemo1
Before: my pid is 3358
After: my pid is 3358, fork() said 3359
After: my pid is 3359, fork() said 0

分辨父进程和子进程

根据不同的进程:fork的返回值是不同的。在子进程中fork返回0,在父进程中fork返回3359。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

main(){
int fork_rv;

printf("Before: my pid is %d\n", getpid());

fork_rv = fork();

if(fork_rv == -1) perror("fork");
else if(fork_rv == 0)
printf("I am child. my pid = %d\n",getpid());
else
printf("I am the parent. my child is %d\n", fork_rv);
}

编译并运行

1
2
3
4
[zhangqi@localhost chap8]$ ./forkdemo3
Before: my pid is 3921
I am the parent. my child is 3922
I am child. my pid = 3922

问题3:父进程如何等待子进程的退出

答案:进程调用wait等待子进程结束

1
2
3
4
5
6
7
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

系统调用wait()做两件事:

  • wait暂停调用它的进程直到子进程结束。
  • wait取得子进程结束时传给exit的值

最终子进程会结束任务并调用exit(n),n是0-255的一个数字

当子进程调用exit,内核唤醒父进程同时将子进程传给exit的参数。唤醒和传递退出(exit)值的动作由exit的括号到父进程的箭头表示。

这样wait执行两个操作:通知和通信

waitdemo1.c显示了子进程调用exit是如何出发wait返回父进程的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

#define DELAY 2

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;
wait_rv = wait(NULL);
printf("done waiting for %d. Wait returned:%d\n",childpid, wait_rv);
}

编译并运行

1
2
3
4
5
[zhangqi@localhost chap8]$ ./waitdemo1 
before:mypid is 6212
child 6213 here, will sleep for 2 seconds
child done, about to exit
done waiting for 6213. Wait returned:6213

运行程序、调整时间,可以发现父进程总会等到子进程调用exit。

在父进程,控制流始于程序的开始,在wait的地方阻塞。在子进程,控制流始于main函数的中部,然后运行child_code函数,最后调用exit结束。

子进程调用exit就像发送一个信号给父进程以唤醒它

这个程序体现了wait的两个重要特征:

  • wait阻塞调用它的程序直到子进程结束。这使两个进程能够同步它们的行为
  • wait返回结束进程的PID

wait还可以告诉父进程子进程是如何结束的(通过wait的入参,该整型变量地址所对应的int会保存子进程的退出状态)。这个整数由三个部分组成:

  • 高八位是记录退出值
  • 低七位记录信号序号
  • 第七位用来指明发生错误并产生了内核映像(core dump)

基于waitdemo1增加DELAY并修改parent_code()函数体部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
void parent_code(int childpid){
int wait_rv;
int child_status;
int high_8, low_7, bit_7;

wait_rv = wait(&child_status);
printf("done waiting for %d. Wait returned:%d\n",childpid, wait_rv);

high_8 = child_status >> 8;
low_7 = child_status & 0x7F;
bit_7 = 0x80;
printf("status: exit = %d, sig = %d, core = %d\n",high_8,low_7,bit_7);
}

运行waitdemo2并正常退出:

1
2
3
4
5
6
[zhangqi@localhost chap8]$ ./waitdemo2
before:mypid is 9820
child 9821 here, will sleep for 5 seconds
child done, about to exit
done waiting for 9821. Wait returned:9821
status: exit = 17, sig = 0, core = 0

运行另一个进程向子进程发送SIGTERM信号(使用kill)

1
2
3
4
5
[zhangqi@localhost chap8]$ ./waitdemo2
before:mypid is 9848
child 9849 here, will sleep for 5 seconds
done waiting for 9849. Wait returned:9849
status: exit = 0, sig = 15, core = 0

小结:shell如何运行程序

  • 用fork建立新进程
  • 用exec在新进程中运行用户指定的程序
  • 用wai等待新进程结束。wait系统调用同时从内核取得退出状态或信号序号以告知子进程是如何结束的

实现一个shell:psh2.c

使用这个简化的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

#define MAXARGS 20
#define ARGLEN 100

extern char *makestring(char argbuf[]);
extern void execute(char *arglist[]);

int main(void) {
char *arglist[MAXARGS + 1];
int numargs = 0;
char argbuf[ARGLEN];
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);
while (--numargs)
free(arglist[numargs]);
numargs = 0;
}
}
}
return EXIT_SUCCESS;
}

extern char *makestring(char argbuf[]) {
argbuf[strlen(argbuf) - 1] = '\0';
char *cp = (char *)malloc(strlen(argbuf) + 1);
if (cp == NULL) {
fprintf(stderr, "no memory\n");
exit(EXIT_FAILURE);
}
strcpy(cp, argbuf);
return cp;
}

extern void execute(char *arglist[]) {
int pid, exitstatus;

pid = fork();
switch(pid) {
case -1:
perror("fork failed");
exit(EXIT_FAILURE);
case 0:
execvp(arglist[0], arglist);
perror("execvp failed");
exit(EXIT_FAILURE);
default:
while (wait(&exitstatus) != pid)
;
printf("child exited with status %d, %d\n",
exitstatus >> 8, exitstatus & 0377);
}
}

还可以继续改进:

  • 让用户可以通过按下Ctrl-D或输入exit退出程序
  • 让用户能够在一行中输入所有参数

用进程编程

考虑函数和进程之间的相似性

execvp/exit就像call/return

  • call/return
    • 一个C程序由很多函数组成,一个函数可以调用另一个函数,同时传给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有它的局部变量,不同的函数通过call/return系统进行通信
    • 这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Unix鼓励将这种应用于程序之内的模式扩展到程序之间
  • exec/exit
    • 一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。
    • 调用它的进程可以通过wait(&result)来获取exit的返回值。
    • 子程序的exit返回值可以在result的8-15位之间找到
    • 由exec传递的参数必须是字符串。由于进程间通信的参数类型为字符串,这样就强迫了子程序的通信也必须使用文本作为参数类型。
  • 全局变量和fork/exec
    • 全局变量是有害的,它破坏了封装原则,但有时又无法去掉全局变量。
    • Unix提供方法来建立全局变量。环境(environment)是一些传递给进程的字符串型变量几个。不会有副作用,它对fork/exec和exit/wait机制是一个有用的补充

exit和exec的其他细节

进程死亡:exit和_exit

  • exit是fork的逆操作,进程通过调用exit来停止运行。fork创建一个进程,exit删除进程
  • exit刷新所有的流,调用由atexit和on_exit注册的函数,执行当前系统定义的其他与exit相关的操作。然后调用_exit。
  • 系统函数_exit是一个内核操作,这个操作处理所有分配给这个进程的内存,关闭所有这个进程打开的文件,释放所有内核用来管理和维护这个进程的数据结构。
  • 子进程传给exit的参数被存放在内核直到这个进程的父进程通过wait系统调用取回这个值。
  • 那些已经死亡但还没有给exit赋值的子进程被称为僵尸(zombie)进程。很多版本的ps将这些进程标记为defunct。
    • 任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将导致没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

_exit()小结:

系统调用_exit终止当前进程并执行所有必须的清理工作,包括:

  • 关闭所有文件描述符和目录描述符
  • 将该进程的PID置为init进程的PID
  • 如果父进程调用wait或waitpid来等待子进程结束,则通知父进程
  • 向父进程发送SIGCHLD
1
2
3
4
5
6
7
#include <unistd.h>

void _exit(int status);

#include <stdlib.h>

void _Exit(int status);

如果父进程在子进程之前退出,子进程会成为孤儿进程,为避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的init进程将会接受这些孤儿进程,这一过程也被称为“收养”(re-parenting)

注意,就算父进程没有调用wait,内核也会向他发送SIGCHLD消息。尽管对SIGCHLD消息的默认处理方法是忽略。但如果想响应这个消息,可以设置一个处理函数

exec家族

注意execvp不是一个系统调用,是一个库函数,这个函数通过系统调用execve来调用内核服务。

另外还有其他一些调用execve的函数:

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...);//第一参数为完整路径
int execlp(const char *file, const char *arg, ...);//不以数组形式传参
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

可编程的shell、shell变量和环境

在shell中可以运行程序,而shell本身就是一种编程语言。shell程序,一般称之为shell脚本,是Unix的重要部分,Unix的引导程序和很多管理程序都是用shell脚本。

shell是一个编程语言解释器,这个解释器解释从键盘输入的命令,也解释存储在脚本中的命令序列。

shell脚本包含一系列命令

运行一个脚本就是运行这个文件中的每个命令

示例:

1
2
3
4
5
6
#---this is  comments----
ls
echo the current date/time is
date
echo my name is
whoami

可以把脚本文件名作为参数传给shell来执行

1
2
3
4
5
6
[zhangqi@localhost chap9]$ sh script0 
script0
the current date/time is
Wed Sep 14 04:26:54 CST 2022
my name is
zhangqi

还可以通过设置文件的执行权限,然后输入文件名来执行脚本:

1
2
3
4
5
6
7
[zhangqi@localhost chap9]$ chmod +x script0 
[zhangqi@localhost chap9]$ ./script0
script0
the current date/time is
Wed Sep 14 04:36:18 CST 2022
my name is
zhangqi

对于一个脚本只需要执行一次chmod,可执行位将保持不变直到下一次再改变它,这种方法来启动脚本会更加方便。

sh编程

sh的编程特征:变量、I/O和if..then

以这个script2程序为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! /bin/sh
# script2: areal program with variables, input and control flow

BOOK = $HOME/phonebook.data
echo find what name in phonebook
read NAME
if grep $ NAME $ BOOK > /tmp/pb.tmp
then
echo Entries for $ NAME
cat /tmp/pb.tmp
else
echo No entriess for $ NAME
fi
rm /tmp/pb.tmp

脚本中除了命令之外还包括以下元素:

  • 变量:脚本中可以定义变量。如在script2中定义了名为BOOK和NAME两个变量,并在定义之后使用了它们,用前缀$来取得变量的值。变量名习惯上大写
  • 用户输入:read命令告诉shell要从标准输入中读入一个字符串。可以使用read来创建交互的脚本,也可以从文件或管道中读入数据
  • 控制:这个脚本包括了 if... then ... else ... fi控制语句。其他脚本控制语句还有 while casefor
  • 环境:脚本使用一个名为HOME的变量。HOME的值是主目录的路径。HOME变量是由login程序设置的,可以被login进程的所有子进程使用。
    • HOME变量是多个环境变量(environment variables)中的一个。这些环境变量记录了个性化设置。而这些设置能影响很多程序的行为。
    • 比如TZ变量记录了当前的时区。将TZ设置为“EST5EDT”是那些使用ctime的程序,比如date或ls -l,应该显示美国东部时间

smsh1——命令行解析

对上一章的自编shell进行改进:加入命令行解析的功能,从而可以在一行中输入命令,然后由解析器将命令行拆成字符串数组,以便传给execvp。

shell的主函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(){
char *cmdline, *prompt, * *arglist;
int result;
void setup();

prompt = DFL_PROMPT;
setup();

//从输入流中读取一行进行处理
while((cmdline = next_cmd(prompt, stdin))!=NULL){
if((arglist = splitline(cmdline))!=NULL){
result = execute(arglist);
freelist(arglist);
}
free(cmdline);
}
return 0;
}

其中有三个函数:

  • next_cmd

    从输入流中读入下一条命令。它调用malloc来分配内存以接受任意长度的命令行。碰到文件结束符,返回NULL

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    char *next_cmd(char *prompt, FILE *fp) {
    char *buf;
    int bufspace = 0;
    int pos = 0;
    int c;

    printf("%s", prompt);
    while ((c = getc(fp)) != EOF) {
    if (pos + 1 > bufspace) {
    if (bufspace == 0)
    buf = (char *)emalloc(BUFSIZ);
    else
    buf = (char *)erealloc(buf, bufspace + BUFSIZ);
    bufspace += BUFSIZ;
    }
    if (c == '\n')
    break;
    buf[pos++] = c;
    }
    if (c == EOF && pos == 0)
    return NULL;
    buf[pos] = '\0';
    return buf;
    }
  • splitline

    splitline将一个字符串分解为字符串数组,并返回这个数组。它调用malloc来分配内存以接受任意参数个数的命令行。这个数组由NULL标记结束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    #define is_delim(x) ((x) == ' ' || (x) == '\t')
    char **splitline(char *line) {
    char **arglist;
    int spots = 0;
    int bufspace = 0;
    int argnum = 0;
    char *cp = line;
    char *start;
    int len;

    if (line == NULL)
    return NULL;

    arglist = (char **)emalloc(BUFSIZ);
    bufspace = BUFSIZ;
    spots = BUFSIZ/sizeof(char *);

    while (*cp != '\0') {
    while (is_delim(*cp))
    cp++;
    if (*cp == '\0')
    break;
    if (argnum + 1 >= spots) {
    arglist = (char **)erealloc(arglist, bufspace + BUFSIZ);
    bufspace += BUFSIZ;
    spots += BUFSIZ/sizeof(char *);
    }

    start = cp;
    len = 1;
    while (*++cp != '\0' && !(is_delim(*cp)))
    len++;
    arglist[argnum++] = newstr(start, len);
    }
    arglist[argnum] = NULL;
    return arglist;
    }

    extern void freelist(char **arglist) {
    char **cp = arglist;
    while (*cp)
    free(*cp++);
    free(arglist);
    }

    static char *newstr(char *s, int len) {
    char *rv = (char *)emalloc(len + 1);
    rv[len] = '\0';
    strncpy(rv, s, len);
    return rv;
    }

    static void *emalloc(int size) {
    void *rv;
    if ((rv = malloc(size)) == NULL)
    fatal("out of memory", "", 1);
    return rv;
    }

    static void *erealloc(void *rv, int size) {
    void *new_rv;
    if ((new_rv = realloc(rv, size)) == NULL)
    fatal("realloc failed", "", 1);
    return new_rv;
    }
  • execute

    execute使用fork、execvp和wait来运行一个命令。execute返回命令的结束状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    int execute(char **arglist) {
    if (arglist[0] == NULL)
    return 0;
    int pid = fork();
    int child_status = -1; // The return value, default set to -1 which show error.

    if (pid == -1) {
    perror("fork");
    return child_status;
    }
    else if (pid == 0) {
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    execvp(arglist[0], arglist);
    perror("cannot execute command");
    exit(EXIT_FAILURE);
    }
    else {
    signal(SIGINT, SIG_IGN);
    signal(SIGQUIT, SIG_IGN);
    if (wait(&child_status) == -1)
    perror("wait");
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    return child_status;
    }
    }

shell中的流程控制

if语句做些什么

考虑以下例子,假设考虑每周五做磁盘备份:

1
2
3
4
5
6
[zhangqi@localhost chap9]$ if date|grep Fri
> then
> echo time for backup, Insert tape and press enter
> read x
> tar cvf dev/tape/home
> fi

shell中的if语句的作用与其他语言一致:做条件检测

程序是如何表示成功的:

  • exit(0)代表成功
    • grep程序调用函数exit(0)来表明成功。所有的Unix程序都遵从以0退出表明成功这一惯例。比如diff命令用来比较两个文本文件。如果两个文件相同,diff返回0表明成功。类似的,mv、cp和rm都以相同的方式表明成功
  • 带有else的if语句
    • else部分就像then部分一样,可以包含任意数量的命令,包括其他的if then语句

if语句还有另一个特征,如果if后的条件是一系列命令,那么最后一个命令的exit值被用作这个语句块的条件值,并由此来决定条件是否成立

if的工作流程:

  • shell运行if后的命令(调用execute)
  • shell检查命令的exit状态(从wait函数中得到)
  • exit的状态为0意味着成功,非0意味着事变
  • 如果成功,shell执行then部分的代码
  • 如果失败,shell执行else部分的代码
  • 关键字fi表示if块的结束

在smsh中增加if

增加流程控制后的流程图:

需要增加一层process:通过寻找关键字,比如if then和fi来管理脚本流程,在适当的时候调用fork和exec。process必须记录条件命令的结果以便能够处理then和else块

process如何工作:

  • process将脚本看作一个接一个的代码区域:第一个区域是then代码块,第二个是else代码块,第三个是在if语句之外的代码块。对于不同的区域,shell的处理方法不同

  • if语句之外的区域称为中立区,对于这类区域的代码,简单地读一条,分析一条,执行一条

  • if和then之间的区域,这个区域中shell每执行一套命令就记录下它的退出状态。

  • shell记录当前区域类型,还必须记录在WANT_THEN区域中所执行命令的结果。

process通过3个函数来处理区域问题:

  • is_control_command:返回一个boolean变量告诉process这条命令是脚本语言的一部分还是一条可执行的命令
  • do_control_command:处理关键字if、then和fi。每个关键字都是区域的界标。这个函数更新状态变量并执行必要的操作
  • ok_to_execute:根据当前的状态和条件命令的结果返回一个boolean值,说明能否执行当前命令

smsh2.c (基于smsh1.c,只在调用execute的地方换成调用process

process.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include "smsh.h"

int is_control_command(char*);
int do_control_command(char**);
int ok_to_execute();

int process(char** args){
int rv = 0;
if(args[0] == NULL) rv = 0;
else if(is_control_command(args[0]))
rv = do_control_command(args);
else if(ok_to_execute())
rv = execute(args);
return rv;
}

controlflow.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 注意 syn_err 会重置 if_state, 也就是说遇到语法错误,前面的条件句就全无效。
// 同时还会忽略 then, fi 之后的命令。
#include <stdio.h>
#include "smsh.h"

enum states {NEUTRAL, WANT_THEN, THEN_BLOCK};
enum results {SUCCESS, FAIL};

static int if_state = NEUTRAL;
static int if_result = SUCCESS;
static int last_stat = 0;

int syn_err(char*);

int ok_to_execute(){
//determin the shell should execute a command
//returns: 1 for yes, 0 for no
int rv = 1;

if(if_state == WANT_THEN){
syn_err("then expected");
rv = 0;
}
else if(if_state == THEN_BLOCK &&if_result == SUCCESS)
rv = 1;
else if(if_state == THEN_BLOCK && if_result == FAIL)
rv = 0;
return rv;
}

int is_control_command(char* s){
return (strcmp(s,"if")==0 || strcmp(s,"then")==0||
strcmp(s,"fi") == 0);
}
int do_control_command(char ** args){
char *cmd = args[0];
int rv = -1;

if(strcmp(cmd,"if")==0){
if(if_state != NEUTRAL)
rv = syn_err("if unexpected");
else{
last_stat = process(args+1);
if_result = (last_stat == 0? SUCCESS : FAIL);
if_state = WANT_THEN;
rv = 0;
}
}
else if(strcmp(cmd,"then") == 0){
if(if_state != WANT_THEN)
rv = syn_err("then unexpected");
else{
if_state = THEN_BLOCK;
rv = 0;
}
}
else if(strcmp(cmd,"fi")==0){
if(if_state!=THEN_BLOCK)
rv = syn_err("fi unexpected");
else{
if_state = NEUTRAL;
rv = 0;
}
}
else
fatal("internal error processing:",cmd,2);
return rv;
}

int syn_err(char * msg){
if_state = NEUTRAL;
fprintf(stderr,"syntax error:%s\n",msg);
return -1;
}

编译并运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cc -o smsh2 smsh2.c splitline.c execute.c process.c controlflow.c 
$ ./smsh2
>grep lp /etc/passwd
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
>if grep lp /etc/passwd
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
>then
>echo ok
ok
>fi
>if grep pati /etc/passwd
>then
>echo ok
>fi
>echo ok
ok
>then
syntax error:then unexpected

这个shell已经可以处理if语句了,但是无法处理嵌套的if语句。

shell变量:局部和全局

shell也有变量,可以对其赋值取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[zhangqi@localhost chap9]$ age=7
[zhangqi@localhost chap9]$ echo $age
7
[zhangqi@localhost chap9]$ echo age
age
[zhangqi@localhost chap9]$ echo $age + $age
7 + 7
[zhangqi@localhost chap9]$ read name
fido
[zhangqi@localhost chap9]$ echo hello, $name, how are you
hello, fido, how are you
[zhangqi@localhost chap9]$ ls > $name.$age
[zhangqi@localhost chap9]$ food = muffins
bash: food: command not found...

shell包括两类变量:局部变量和环境变量(HOME、TZ等)。环境变量可以被所有shell的子进程存取

使用shell变量

注意变量的值是字符串,没有数值类型的变量。所有的操作都是字符串操作

可以使用set命令列出当前shell定义的所有变量:

变量的存储

要在shell里增加变量,必须有个地方能存放这些变量的名称和值,而且这个变量存储系统必须能够分辨局部和全局变量

下面是这个系统的抽象模型:

接口:

I/O重定向和管道

考虑这组命令:

1
2
3
4
[zhangqi@localhost chap10]$ ls > my.files
[zhangqi@localhost chap10]$ who | sort > userlist
[zhangqi@localhost chap10]$ ls
my.files userlist

shell是如何告诉程序将结果输出到文件而不是屏幕的?又是如何将一个进程的输出流连接到另一个进程的输入流的?标准输入(standard imput)这个术语是什么意思?

本章涉及进程间通信的一种特殊形式:输入/输出重定向和管道(I/O redirection and pipes)

监控系统用户的shell程序

考虑一个情景:希望编写一个程序,当其他用户登录系统或注销时通知你。

一种思路:可以使用utmp文件和建个计数器的C程序来完成任务。程序打开utmp文件,记录下用户列表,休眠一段时间后再重新扫描此文件,并将变化报告出来

更简单的办法:写一个shell脚本。Unix中通过who命令列出当前用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# whos.sh

who | sort>prev
while true;
do
sleep 60
who | sort>curr
echo "logged out"
comm -23 prev curr #删除第二列和第三列=》仅显示prev中的内容
echo "logged in"
comm -13 prev curr #删除第一列和第二列=》仅显示curr中的内容
mv curr prev
done

此脚本使用了Unix提供的7个工具、一个while循环和I/O重定向:

  • 第一行建立了一个在此脚本运行时已登录用户的列表,并按用户名进行排序。who命令输出用户列表,sort命令将列表作为输入读进,然后输出一个排好序的列表

    • 这个命令告诉shell同时执行who和sort,将who的输出直接送到sort的输入

    • who命令并不一定要在sort命令开始读取和排序之前完成对utmp文件的分析。这两个进程以很小的时间间隔为单位来调度,他们和系统中的其他进程一起分享CPU时间。

  • 休眠一分钟后,脚本在文件curr中创建了一个新的用户列表。使用comm工具可以找出两个文件中共有的行。比较两个文件可以得到三个子集:

    • 仅文件1有的行
    • 仅文件2有的行
    • 两者共有的行
    • comm命令比较了两个排过序的列表,并将此三列打印出来,这里的每一列代表一个子集的内容

这个脚本体现了三个重要思路:

  • shell脚本的功能——与C相比简单易用
  • 软件工具的灵活性——每一个工具完成一项特定的、通用的功能
  • I/O重定向和管道的使用和作用

使用>操作符可以把文件看成任意大小和结构的变量,比如C的调用:

1
x = func_a(func_b(y));

用shell写就是:

1
prog_b | prog_a > x #将prog_b的结果作为prog_a的输入并将最终结果放入x

标准I/O与重定向的若干概念

所有的Unix I/O重定向都基于标准数据流的原理。考虑sort工具是如何工作的。

sort从一个数据流中读取字节,再将结果输出到另一个流中,同时若有错误发生,则将错误报告给第三个流。如果忽略这些标准流的去向问题,sort工具的基本原型就如图所示:

三个数据流分别如下:

  • 标准输入——需要处理的数据流
  • 标准输出——结果数据流
  • 标准错误输出——错误消息流

概念1: 3个标准文件描述符

三种流的每一种都是一个特别的文件描述符:

  • stdin:0
  • stdout:1
  • stderr:2

默认的连接:tty

通常通过shell命令行运行Unix系统工具时,stdin、stdout和stderr连接在终端上。因此,工具从键盘读取数据并且把输出和错误消息写到屏幕。

举例来说:如果输入sort并按下回车键,终端将会被连接到sort工具上。随便输入几行文字,当按Ctrl-D键来结束文字输入的时候,sort程序对输入进行排序,并将结果写到stdout。

大部分的Unix工具处理从文件或标准输入读入的数据。如果在命令行上给出了文件名,工具将从文件读取数据。若无文件名,程序则从标准输入读取数据

程序都输出到stdout

另一方面说,大多数程序并不接收输出文件名;他们总将结果写到文件描述符1,并将错误消息写到文件描述符2。如果希望将进程的输出写到文件或另一个进程的输入去,就必须重定向相应的文件描述符。

重定向I/O的是shell而不是程序

通过使用输出重定向标志,命令cmd>filename告诉shell将文件描述符1定位到文件,于是shell就将文件描述符与指定的文件连接起来。

程序则持续不断地将数据写到文件描述符1中,没有意识到数据的目的地已经改变了

如下面的程序甚至没有看到命令行中的重定向符号:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

main(int ac, char * av[]){
int i;

printf("Number of args: %d, Args are: \n",ac);
for(i = 0;i < ac; ++i)
printf("args[%d] %s\n",i, av[i]);

fprintf(stderr, "This message is sent to stderr.\n");
}

程序将命令行参数打印到标准输出。注意它并没有打印出重定向符号和文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[zhangqi@localhost chap10]$ ./listargs
Number of args: 1, Args are:
args[0] ./listargs
This message is sent to stderr.
[zhangqi@localhost chap10]$ ./listargs testing one two
Number of args: 4, Args are:
args[0] ./listargs
args[1] testing
args[2] one
args[3] two
This message is sent to stderr.
[zhangqi@localhost chap10]$ ./listargs testing one two > xyz
This message is sent to stderr.
[zhangqi@localhost chap10]$ cat xyz
Number of args: 4, Args are:
args[0] ./listargs
args[1] testing
args[2] one
args[3] two
[zhangqi@localhost chap10]$ ./listargs testing one two > xyz one two 2>oops
[zhangqi@localhost chap10]$ cat xyz
Number of args: 6, Args are:
args[0] ./listargs
args[1] testing
args[2] one
args[3] two
args[4] one
args[5] two
[zhangqi@localhost chap10]$ cat oops
This message is sent to stderr.

这些例子验证了关于shell输出重定向的一些概念:

  • 最重要的一点是shell不将重定向标记和文件名传递给程序
  • 第二点是重定向可以出现在命令行中的任何地方,并且在重定向标识符周围并不需要空格来区分。
    • 类似 >listing ls这样的命令也是可以接受的,这里的 >符号并不能终止命令和参数,它只不过是一个附加的请求而已。
  • 另外许多版本的shell都提供对重定向其他文件描述符的支持。如 2>filename即重定向文件描述符2,也就是将标准错误输出到给定的文件中

三个基本的重定向操作程序:

1
2
3
who>userlist   	#将stdout连接到一个文件
sort<data #将stdin连接到一个文件
who|sort #将stdout连接到stdin

文件描述符

文件描述符是一个数组的索引号。每个进程都有其打开的一组文件。这些打开的文件被保持在一个数组中。文件描述符即为某文件再次数组中的索引。

下图展示了“最低可用文件描述符(Lowest-Available-fd”)原则

概念:当打开文件时,为此文件安排的描述符总是次数组中最低可用位置的索引

程序如何将stdin定向到文件

精确的说,进程并不是从文件读数据,而是从文件描述符读数据,如果将文件描述符0定位到一个文件,那么此文件就成为标准输入的源

以下是三种将标准输入定位到文件的方法:

  1. close then open:

    • 初始情况,系统中三种标准流分别连接到文件描述符0,1,2

    • 第一步close(0),将标准输入的连接挂断

      image-20220916071733439
    • 此时文件描述符数组中的第一个元素现在处于空闲状态

    • 最后使用 open(filename, O_RDONLY)打开一个想连接到stdin上的文件。由于当前最低可用文件描述符是0,因此打开的文件将被连接到标准输入

    • 代码实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      #include <stdio.h>
      #include <fcntl.h>

      main(){
      int fd;
      char line[100];

      //read and print 3 lines
      fgets(line,100,stdin);printf("%s",line);
      fgets(line,100,stdin);printf("%s",line);
      fgets(line,100,stdin);printf("%s",line);

      //redirect input
      close(0);
      fd = open("/etc/passwd",O_RDONLY);
      if(fd!=0){
      fprintf(stderr, "Could not open data as fd 0\n");
      exit(1);
      }
      //read and print 3 lines
      fgets(line,100,stdin);printf("%s",line);
      fgets(line,100,stdin);printf("%s",line);
      fgets(line,100,stdin);printf("%s",line);
      }

      该程序从标准输入读取并打印了三行字符串,然后重定向标准输入,之后又从标准输入中读取并打印了三行字符串(后三行字符串是从passwd中读出的)。

  2. open.. close.. dup.. close

    Unix系统调用dup建立指向已经存在的文件描述符的第二个连接,这个方法需要4个步骤:

    • open(file):先打开stdin要重定向的文件,这个调用会返回一个文件描述符(非0)
    • close(0):文件描述符0现在空闲
    • duo(fd):将文件描述符fd做一个复制,此次复制使用最低可用文件描述符号,因此获得的文件描述符为0。
    • close(fd):关闭文件的原始连接,只留下文件描述符0的连接

    代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #include <stdio.h>
    #include <fcntl.h>

    main(){
    int fd;
    int newfd;
    char line[100];

    //read and print 3 lines
    fgets(line,100,stdin);printf("%s",line);
    fgets(line,100,stdin);printf("%s",line);
    fgets(line,100,stdin);printf("%s",line);

    //redirect input
    fd = open("/etc/passwd",O_RDONLY);
    #ifdef CLOSE_DUP
    close(0);
    newfd = dup(fd);
    #else
    newfd = dup2(fd,0);
    #endif
    if(newfd!=0){
    fprintf(stderr, "Could not open data as fd 0\n");
    exit(1);
    }

    //read and print 3 lines
    fgets(line,100,stdin);printf("%s",line);
    fgets(line,100,stdin);printf("%s",line);
    fgets(line,100,stdin);printf("%s",line);
    }

    dup系统调用

    1
    2
    3
    4
    5
    #include <unistd.h>

    int dup(int oldfd); //oldfd为需要复制的文件描述符
    int dup2(int oldfd, int newfd); //newfd为复制oldfd后得到的文件描述符
    //发生错误,返回-1。成功,返回newfd
  3. open..dup2..close

    已包含在上面代码中

这些例子显示了程序如何将标准输入重定向到文件。但事实上,如果程序希望读取文件,它直接打开文件就可以了。这些例子的意义在与说明一个程序如何将标准输入重定向到别的程序。

为其他程序重定向I/O: who > userlist

当某用户输入 who>userlist,shell运行who程序,并将who的标准输出重定向到名为userlist的文件上。这是如何完成的呢?

关键就在与fork和exec之间的时间间隙。在fork执行之后,子进程仍然在运行shell程序,并准备执行exec。exec将替换进程中运行的程序,但它不会改变进程的属性和进程中所有的连接。

也就是说,在运行过exec之后,进程的用户ID不会改变,其优先级不会改变,并且其文件描述符也和运行exec之前一样。注意,程序得到的是载入它的进程所打开的文件。

  1. 初始情况

    如图所示,进程运行在用户空间中。文件描述符1连接在打开的文件f上。

  2. 父进程调用fork之后

    新进程出现,与原始进程运行相同的代码。但它知道自己是子进程。然后子进程会调用close(1)

  3. 子进程调用close(1)之后

    由于父进程没有调用close(1),因此父进程的文件描述符1仍然指向f。子进程调用close(1)后,文件描述符1变成了最低未用文件描述符

  4. 在子进程调用 creat("g",m)之后

    文件描述符1被连接到文件g、子进程的标准输出被重定向到g。子进程然后调用exec来运行who

  5. 子进程使用exec执行新程序之后

    子进程执行who程序,于是子进程中的代码和数据都被who程序的代码和数据替代了,然而文件描述符被保留下来。打开的文件并非是程序的代码也不是数据,它们属于进程的属性,因此exec调用并不改变它们。

who命令将当前用户列表送至文件描述符1。其实这组字节已经被写到文件g中去了,而who命令毫不知情

下面程序实现了上面说的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>

main(){
int pid;
int fd;
printf("About to run who into a file\n");

//create a new process or quit
if((pid = fork())== -1){
perror("fork");
exit(1);
}
//child does the work
if(pid == 0){
close(1);
fd = creat("userlist",0644);
execlp("who","who",NULL);
perror("execlp");
exit(1);
}
//parent waits then reports
if(pid!=0){
wait(NULL);
printf("Done running who. results in userlist\n");
}
}

管道编程

本节讨论如何使用管道来连接一个进程的输出和另一个进程的输入。

管道是内核中的一个单向的数据通道。管道有一个读取端和一个写入段。

管道编程涉及:如何创建管道和如何将标准输入和输出通过管道连接起来

创建管道

1
2
3
4
#include <unistd.h>

int pipe(int pipefd[2]); //包含两个int类型数据的数组
//发生错误返回-1,成功返回0

调用pipe来创建管道并将其两端连接到两个文件描述符。

array[0]存放数据端的文件描述符,而array[1]存放写数据端的文件描述符。像一个打开的文件的内部情况一样,管道的内部实现隐藏在内核中,进程只能看见两个文件描述符。

下图显示了进程创建一个管道前后的状况。前一张图(调用pipe之前)显示了标准文件描述符集。后一张图(调用pipe之后)显示了内核中新创建的管道,以及进程到管道的两个连接。注意,类似于open调用,pipe调用也使用最低可用文件描述符。

下面程序展示了创建管道并使用管道向自己发送数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void) {
int len, apipe[2];
char buf[BUFSIZ];

if (pipe(apipe) == -1) {
perror("create a pipe failed");
exit(EXIT_FAILURE);
}
printf("Got a pipe! It is file descriptors: {%d %d}\n", apipe[0], apipe[1]);

while (fgets(buf, BUFSIZ, stdin)) {
len = strlen(buf);

if (write(apipe[1], buf, len) != len) {
perror("write to a pipe failed");
break;
}

for (int i = 0; i < len; ++i) {
buf[i] = 'x';
}

len = read(apipe[0], buf, len);
if (len == -1) {
perror("read from a pipe failed");
break;
}

if (write(1, buf, len) != len) {
perror("write to stdout failed");
break;
}
}
return EXIT_SUCCESS;
}

数据流从进程到管道,再从管道到进程以及从进程回到终端

将pipe和fork结合起来,就可以连接两个不同的进程了

使用fork来共享管道

当进程创建一个管道之后,该进程就有了连向管道两端的连接。当这个进程调用fork的时候,它的子进程也得到了这两个连向管道的连接。

如图,父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。

两个进程都可以读写管道,但是当一个进程读,另一个进程写的时候,管道的使用效率最高

下面程序将pipe和fork结合起来,创建一对通过管道来通信的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define CHILD_MESS "I want a cookie\n"
#define PARENT_MESS "testing...\n"
#define oops(m,x) {perror(m); exit(x);}

int main(void) {
int len, read_len, pipefd[2];
char buf[BUFSIZ];

if (pipe(pipefd) == -1)
oops("cannot get a pipe", 1);

switch(fork()) {
case -1:
oops("cannot fork", 2);
case 0:
len = strlen(CHILD_MESS);
while (1) {
if (write(pipefd[1], CHILD_MESS, len) != len)
oops("cannot", 3);
sleep(5);
}
default:
len = strlen(PARENT_MESS);
while (1) {
if (write(pipefd[1], PARENT_MESS, len) != len)
oops("write", 4);
sleep(1);
read_len = read(pipefd[0], buf, BUFSIZ);
if (read_len <= 0)
break;
write(1, buf, read_len);
}
}

return EXIT_SUCCESS;
}

使用pipe、fork以及exec

将所有技巧结合在一起,编写一个通用的程序pipe,它使用两个程序的名字作参数,如:

1
2
pipe who sort
pipe ls head

程序内在逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "usage: pipe cmd1 cmd2\n");
exit(EXIT_FAILURE);
}

int pipefd[2];
pipe(pipefd); // pipe 一定要在 fork 之前执行……
int pid = fork();

if (pid == -1) {
fprintf(stderr, "fork failed\n");
exit(EXIT_FAILURE);
}
else if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], 1);
close(pipefd[1]); // 强力关掉无用的管道 fd 是好习惯
execlp(argv[1], argv[1], NULL);
fprintf(stderr, "exec cmd 1 failed\n");
exit(EXIT_FAILURE);
}
else {
close(pipefd[1]);
dup2(pipefd[0], 0);
close(pipefd[0]); // 强力关掉无用的管道 fd 是好习惯
execlp(argv[2], argv[2], NULL);
fprintf(stderr, "exec cmd 2 failed\n");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

技术细节:管道并非文件

管道与文件的相同点与不同点

  1. 从管道中读数据
    • 管道读取阻塞:当进程试图从管道中读数据时,进程被挂起知道数据被写进管道。那么如何避免进程永无止境地等待下去呢?
    • 管道的读取结束标志:当所有的写者关闭了管道的写数据端时,试图从管道读取数据的调用返回0,这意味着文件的结束
    • 多个读者可能会引起麻烦:管道是一个队列。当进程从管道中读取数据之后,数据已经不存在了。如果两个进程都试图对同一个管道进行读操作,在一个进程读取一些之后,另一个进程读到的将是后面的内容。除非两个进程使用某种方法来协调它们对管道的访问。
  2. 向管道中写数据
    • 写入数据阻塞直到管道有空间去容纳新数据:管道容纳数据的能力要比磁盘文件差得多。当进程试图对管道进行写操作的时候,此调用将挂起进程直到管道中有足够的空间去容纳新的数据。
    • 写入必须保证一个最小的块大小:POSIX标准规定内核不会拆分小于512字节的块。而Linux则保证管道中可以存在4096字节的连续缓存。如果两个进程向管道写数据,并且每一个进程都限制其消息不大于512字节,那么这些消息都不会被内核拆分
    • 若无读者在读取数据,则写操作执行失败:如果所有的读者都已将管道的读取端关闭,那么对管道的写入调用将会执行失败。

连接到近端或远端的进程:服务器与Socket

Unix提供一个接口来处理可能来自不同数据源的数据

image-20220916232108753
  • 磁盘/设备文件:用open命令连接,用read和write传递数据
  • 管道:用pipe命令创建,用fork共享,用read和write传递数据
  • sockets:用socket、listen和connect连接,用read和write传递数据

以Unix中的计算器bc为例

1
2
3
4
5
6
7
8
9
[zhangqi@localhost chap10]$ bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
17^123
22142024630120207359320573764236957523345603216987331732240497016947\
29282299663749675090635587202539117092799463206393818799003722068558\
0536286573569713

bc并不是一个计算器

一个计算器程序分析输入,执行操作,打印输出。但是大部分版本的bc程序都只分析输入,不执行操作,取而代之的,bc在内部启动了dc计算器程序,并通过管道与其进行通信。

dc是一个基于栈的计算器,它需要用户在指定具体的操作符之前,先输入所有操作的数据(如用户输入 2 2 + 来代表2+2的操作

下图显示了整个过程:

  • 用户输入2+2,然后回车
  • bc从标准输入读取该表达式,分析出数据和操作符
  • 接下来把一系列命令 22+p传给dc(p是结束符)
  • dc将数据入栈,运行加操作
  • 最后把栈顶的数值送到标准输出

bc从连接到dc标准输出的管道上读取结果,再把结果转发给用户。这样,bc甚至不需要持有变量。

如果用户输入 x = 2+2,bc告诉dc执行该操作并且把结果存到寄存器x中。命令bc -c可以显示分析器传给计算器的数据。

从bc中得到的思想

  1. 客户/服务器模型
    • bc/dc程序是对C/S模型程序设计的一个实例。dc提供服务:计算。
    • dc所标识的语言是逆波兰表示法。bc和dc之间通过标准输入stdin和标准输出stdout进行通信
    • bc提供用户界面,并使用dc提供的服务。bc被称为dc的客户
    • 这两部分是根本上独立的程序
  2. 双向通信
    • C/S模型不同于生产线的数据处理模型,它要求一个进程既跟另一个进程的标准输入也要和它的标准输出进行通信。
    • 传统的Unix的管道只是单方向地传送数据。如上图中bc和dc之间的两个管道,上面的管道把一些计算命令传给dc的标准输入,下面的管道把dc的标准输出传给bc
  3. 永久性服务
    • bc只是让单一的dc进程处于运行状态,这就不同于shell程序,这种程序中的每个用户命令都创建一个新的进程。
    • bc程序持续不断地和dc的同一个实例进行通信,把用户的输入转换成命令传给dc。
    • 他们之间的关系并不同于标准函数中所使用的调用返回机制
    • bc/dc对被称之为协同进程(coroutines)以用来区别于子程序(subroutines)

编写bc:pipe、fork、dup、exec

流程:

  • 创建两个管道
  • 创建一个进程来运行dc
  • 在新创建的进程中,重定向标准输入和标准输出到管道,然后运行exec dc
  • 在父进程中,读取并分析用户的输入,将命令传给dc,dc读取响应,并把响应传给用户

以下简单实现了一个bc(只能对两个数做计算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//tinybc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define oops(m,x) {perror(m);exit(x);}

main(){
int pid, todc[2], fromdc[2]; //todc存储从bc到dc管道的读fd和写fd,fromdc相反

//make 2 pipes
if(pipe(todc) == -1 || pipe(fromdc) == -1)
oops("pipe failed",1);

if((pid = fork()) == -1) //fork调用一次,返回两次,父进程中返回子进程的进程id,子进程中返回0
oops("cannot fork",2);

if(pid == 0)//child is dc
be_dc(todc,fromdc);
else{
be_bc(todc,fromdc);
wait(NULL);
}
}

be_dc(int in[2], int out[2]){
//在子进程dc中处理bc通过todc[0]的数据,计算,再从fromdc[1]输出
//set up stdin and stdout, then execl dc

//set up stdin from pipe in
if(dup2(in[0],0) == -1) //copy read end to 0
oops("dc: cannot redirect stdin", 3);
close(in[0]); //close old fd, newfd is fd 0
close(in[1]); //won't write here

//set up stdout to pipeout
if(dup2(out[1],1) == -1)
oops("dc: cannot redirect stdout",4);
close(out[1]); //moved to fd 1
close(out[0]); //won't read from here
//now execl dc with the - option
execlp("dc", "dc", "-", NULL);
oops("Cannot run dc",5);
}

be_bc(int todc[2], int fromdc[2]){
int num1, num2;
char operation[BUFSIZ], message[BUFSIZ], *fgets();
FILE *fpout, *fpin, *fdopen();

close(todc[0]); //won't read from pipe to dc
close(fromdc[1]); //won't write to pipe from dc
fpout = fdopen(todc[1],"w");
fpin = fdopen(fromdc[0],"r");
if(fpout == NULL || fpin == NULL)
fatal("Error converting pipes to streams");

//main loop
while(printf("tinybc: "),fgets(message, BUFSIZ, stdin)!=NULL){
//parse input
if(sscanf(message,"%d %[- + */^]%d",
&num1,operation,&num2)!=3){
printf("syntax error\n");
continue;
}

if(fprintf(fpout, "%d\n%d\n%c\np\n",num1,num2,*operation) == EOF)
fatal("Error writing");
fflush(fpout);
if(fgets(message,BUFSIZ,fpin)==NULL)
break;
printf("%d %c %d = %s",num1,*operation, num2,message);
}
fclose(fpout);
fclose(fpin);
}
fatal(char * mess[]){
fprintf(stderr,"Error: %s\n",mess);
exit(1);
}

编译并运行

1
2
3
4
5
6
7
[zhangqi@localhost chap11]$ cc tinybc.c -o tinybc
[zhangqi@localhost chap11]$ ./tinybc
tinybc: 2+2
2 + 2 = 4
tinybc: 55^5
55 ^ 5 = 503284375
tinybc:

注意tinybc.c中使用了库函数fdopen:

  • fdopen与fopen类似,返回一个FILE *类型的值,不同的是此函数以文件描述符而非文件作为参数
  • 使用fopen的时候,将文件名作为参数传给它,fopen可以打开设备文件也可以打开常规的磁盘文件。如只知道文件描述符而不清楚文件名的时候可以使用fdopen命令。
  • 注意tinybc.c使用fprintf和fgets来通过管道和dc进行通信的方式
  • 使用fdopen使得对远端的进程的处理就如同处理常规文件一样

popen:让进程看似文件

fopen打开一个指向文件的带缓存的连接:

1
2
3
4
5
6
File * fp;				//a pointer to a struct
fp = fopen("file1","r");//args are filename, connection type
c = getc(fp); //read char by char
fgets(buf, len, fp); //line by line
fscanf(fp,"%d %d %s",&x,&y,x);//token by token
fclose(fp); //close when done

connection type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
r      Open text file for reading.  The stream is positioned at the beginning of the file.

r+ Open for reading and writing. The stream is positioned at the beginning of the file.

w Truncate file to zero length or create text file for writing. The stream is positioned
at the beginning of the file.

w+ Open for reading and writing. The file is created if it does not exist, otherwise it
is truncated. The stream is positioned at the beginning of the file.

a Open for appending (writing at end of file). The file is created if it does not exist.
The stream is positioned at the end of the file.

a+ Open for reading and appending (writing at end of file). The file is created if it
does not exist. The initial file position for reading is at the beginning of the file,
but output is always appended to the end of the file.

popen看上去跟fopen很类似(使用相同的语法格式,并具有相同的返回值类型),popen打开一个指向进程的带缓冲的连接

1
2
3
4
File * fp;				//same type of struct
fp = popen("ls","r"); //args are connection name, connection type
fgets(buf, len, fp); //exactly the same functions
pclose(fp); //close when done

下面的程序将who|sort作为数据源,通过popen来获得当前用户排序列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>

int main(){
FILE* fp;
char buf[100];
int i=0;

fp = popen("who|sort","r");

while(fgets(buf,100,fp)!=NULL)
printf("%3d %s",i++,buf);

pclose(fp);
return 0;
}

下面的程序将popen和邮件程序相连接,用来提示用户一些系统故障

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>

int main(void) {
FILE *fp = popen("mail admin backup", "w");

fprintf(fp, "Error with backup!\n");
pclose(fp);
return EXIT_SUCCESS;
}

注意pclose命令是必须的。

当完成对popen所打开连接的读写后,必须使用pclose关闭连接,而不能用fclose。

进程在产生之后必须等待退出运行,否则它将成为僵尸进程。而pclose中调用了wait函数来等待进程的结束。

实现popen:使用fdopen命令

popen运行了一个程序并返回指向改程序标准输入或标准输出的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define READ 0
#define WRITE 1

FILE *popen(const char *command, const char *mode) {
int p[2],pid;
int parent_end, child_end;

if (*mode == 'r') {
parent_end = READ;
child_end = WRITE;
}
else if (*mode == 'w') {
parent_end = WRITE;
child_end = READ;
}
else
return NULL;

if(pipe(p)==-1)
return NULL;

if((pid = fork())==-1){
close(p[0]);
close(p[1]);
return NULL;
}

if (pid > 0) {
if(close(p[child_end]) == -1)
return NULL;
return fdopen(p[parent_end], mode);
}

close(p[parent_end]);
dup2(p[child_end], child_end);
close(p[child_end]);

execl("/bin/sh", "sh", "-c", command, NULL);
return EXIT_SUCCESS;
}

改版本的popen对信号不做任何处理,这是有问题的

访问数据:文件、应用程序接口(API)和服务器

以获取登录系统的用户列表为例:

  • 方法1:fopen从文件获得数据

    之前的章节所写的who程序就是从utmp文件中读取数据的。基于文件的信息服务并不是很完美。客户端程序依赖于特定的文件格式和结构体中的特定成员名称。

    1
    2
    /*Backwards compatibility hacks*/
    #define ut_name ut_user
  • 方法2:从函数获取数据

    可以通过调用函数来得到数据。一个库函数用标准的函数接口来封装数据的格式和位置。Unix提供了读取utmp文件的函数接口。getutent的版主信息描述了读取utmp数据库函数的细节。

    这样,就算底层的存储结构变化了,使用这个借口的程序仍能正常工作

  • 方法3:从进程获取数据

    bc/dc和popen例子显示了如何创建一个进程到另外一个进程的连接。一个要得到用户列表的程序可以使用popen来建立与who程序的连接。由who命令来负责使用正确的文件名和文件格式以及正确的库函数。

    以独立的程序获得数据有以下好处:

    • 服务器程序可以使用任何程序设计语言编写:shell、C、Java、Perl
    • 最大好处是客户端程序和服务端程序可以运行在不同的机器上

Socket:与远端进程相连

管道使得进程向其他进程发送数据就像向文件发送数据一样容易,但是管道具有两个重大缺陷:

  • 管道在一个进程中被创建,通过fork来实现共享。因此管道只能连接相关的进程,也只能连接同一台主机上的进程

Unix提供了另外一种进程间的通信机制——socket

socket允许在不想管的进程间创建类似管道的连接,甚至可以通过socket连接其他主机上的进程。

重要概念:

  1. 客户端和服务器:
    • 服务器是提供服务的程序。在Unix中,服务器是一个程序不是一台计算机。服务器进程等待请求,处理请求,然后循环回去等待下一个请求
    • 客户端进程则不需要循环,它只需建立一个连接,与服务器交换数据,然后继续自己的工作。
  2. 主机名和端口
    • 运行于因特网上的服务器其实是某台计算机上运行的一个进程。这里计算机被称为主机
    • 机器通常被指定一个名字如sales.xyzcorp.com,这被称为该主机的名字。
    • 服务器在该主机上拥有一个端口。主机和端口的组合才标识了一个服务器
  3. 地址族(AF,address family)
  4. 协议:协议是服务器和客户端之间交互的规则

编写自己的时间服务器

设计6个步骤:

  • socket
  • bind
  • listen
  • accept
  • read/write
  • close

socket()相关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
//domain:通信域,其中PF_INET用于Internet socket
//type:socket的类型。SOCK_STREAM和管道类似
//protocol:socket中所用的协议,默认为0

//遇到错误,返回-1,成功,返回sockid

DESCRIPTION
socket() creates an endpoint for communication and returns a descriptor.

The domain argument specifies a communication domain; this selects the protocol family which
will be used for communication. These families are defined in <sys/socket.h>. The currently
understood formats include:

Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <time.h>
#include <strings.h>
#include <stdlib.h>

#define PORTNUM 13000
#define HOSTLEN 256
#define oops(msg) {perror(msg);exit(1);}

int main(int ac, char * av[]){
struct sockaddr_in saddr; //build our address here
struct hostent *hp; //this is part of our
char hostname[HOSTLEN]; //address
int sock_id,sock_fd; //line id, file desc
FILE *sock_fp; //use socket as stream
char *ctime(); //convert secs to string
time_t thetime; //the time we report

//step1: ask kernel for a socket
sock_id = socket(PF_INET, SOCK_STREAM,0);
if(sock_id == -1)
oops("socket");

//step2:bind address to socket. Address is host,port
bzero((void *)&saddr, sizeof(saddr)); //clear out struct

gethostname(hostname,HOSTLEN); //where am I?
//printf("hostname: %s",hostname);
hp = gethostbyname(hostname); //get info about host

//fill in host part
bcopy((void*)hp->h_addr,(void*)&saddr.sin_addr,hp->h_length);
saddr.sin_port = htons(PORTNUM); //fill in socket port
saddr.sin_family = AF_INET; //fill in addr family
oops("bind");

//step3:allow incoming calls with Qsize = 1 on socket
if(listen(sock_id,1)!=0)
oops("listen");

//main loop: accept(),write(),close()
while(1){
sock_fd = accept(sock_id,NULL,NULL); //wait for call
printf("Got a call!\n");
if(sock_fd == -1)
oops("accept");

sock_fp = fdopen(sock_fd,"w"); //will write to the socket as a stream
if(sock_fp == NULL)
oops("fdopen");

thetime = time(NULL); //get times and convert to string

fprintf(sock_fp,"The time here is ..");
fprintf(sock_fp,"%s",ctime(&thetime));
fclose(sock_fp); //release connection
}
close(sock_id);
return 0;
}

流程:

步骤1:向内核申请一个socket

1
2
3
4
5
6
7
8
9
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
//domain:通信域,其中PF_INET用于Internet socket
//type:socket的类型。SOCK_STREAM和管道类似
//protocol:socket中所用的协议,默认为0

//遇到错误,返回-1,成功,返回sockid
  • socket调用创建一个通信端点并返回一个标识符。有很多种类型的通信系统,每个被称为一个通信域。Internet本身就是一个域。在后面会看到Unix内核是另一个域。Linux支持好几个其他域的通信
  • socket的类型指出了程序将要使用的数据流类型。SOCK_STREAM类型和双向管道类似。数据作为连续的字节流从一端写入,再从另一端读出。
  • protocol值得是内核中网络代码所使用的的协议,并不是客户端和服务器之间的协议。一个为0的值代表选择标准的协议

步骤2:bind绑定地址到socket上,地址包括主机,端口

1
2
3
4
5
6
7
8
9
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
//sockfd为socket的id
//addr为指向包含地址结构的指针
//addrlen为地址的长度
//成功返回0,失败返回-1
  • 注意需要使用没被占用的端口(低端口号可能已经被系统服务所占用,不能被普通用户所使用)
  • 每个地址族都有自己的格式,因特网地址族(AF_INET)使用主机和端口来标志。地址就是一个以主机和端口为成员的结构体。
  • 自己写的程序应首先初始化该结构的成员,然后再填充具体的主机地址和端口号,最后填充地址族。
  • 当所有的部分被填充了之后,地址已经被绑定到该socket上。其他类型的socket会使用包含不同成员的地址

步骤3:在socket上,允许接入呼叫并设置队列长度为1。listen

1
2
3
4
5
6
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
//backlog:允许接入连接的数目
//成功,返回0,错误,返回-1
  • listen请求内核允许指定的socket接收接入呼叫。
  • 注意不是所有类型的socket都能接收接入呼叫。但SOCK_STREAM类型是可以的
  • 第二个参数指出接收队列的长度,在这里使用长度为1的队列。队列最大长度取决于具体socket的实现

步骤4:等待/接收呼叫 accept

1
2
3
4
5
6
7
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//addr:指向呼叫者地址结构的指针
//addelenp:指向呼叫者地址结构长度的指针
//遇到错误,返回-1,正确,返回fd(用于读写的文件描述符)
  • 一旦socket被建立并被分配一个地址,而且准备等待接收呼叫,程序即将开始工作。服务器等待直到呼叫到来。
  • accept阻塞当前进程,一直到指定socket上的接入连接被建立起来,然后accept将返回文件描述符,并用该文件描述符来进行读写操作。此文件描述符实际上是连到呼叫进程的某个文件描述符的一个连接
  • accept支持一种类型的呼叫者的ID。在呼叫发起者一边,socket有自己的地址,例如对应因特网连接,地址就是主机地址和端口号。如果addr和addrlen指针不为空的话,内核将把呼叫者地址填充到addr所指向的结构中,并把该结构的长度填充到addrlen所指向的内存单元中。
  • 一个网络程序可以使用呼叫进程的地址来决定如何处理该连接

步骤5:传输数据

  • accept调用所返回的文件描述符是一个普通文件的描述符。
  • 可以使用相关的操作进行读写。我们的程序用fdopen将文件描述符定向到缓存的数据流,以便于使用fprintf调用来进行输出。

步骤6:关闭连接

  • accept所返回的文件描述符可以由标准的系统调用close关闭。当一段的进程关闭了该端的socket,若另一端的进程在试图读数据的话,它将得到文件结束标记。这跟管道的工作原理类似

测试timeserv.c

编译并运行:(启动读物以&号结尾

1
2
3
4
5
6
7
8
9
[zhangqi@localhost chap11]$ ./timeserv&
[1] 91871
[zhangqi@localhost chap11]$ telnet '127.0.0.1' 13000
Trying 127.0.0.1...
Got a call!
Connected to 127.0.0.1.
Escape character is '^]'.
The time here is ..Sat Sep 17 04:35:06 2022
Connection closed by foreign host.

可见服务器响应了连接

服务器将持续运行,直到使用kill命令来结束其运行

1
2
[zhangqi@localhost chap11]$ kill 91871
[1]+ Terminated ./timeserv

编写timeclient.c:时间服务客户端

时间服务客户端的实现包含4个步骤:

  • socket
  • connect
  • read/write
  • close

程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#define oops(msg) {perror(msg);exit(1);}

main(int ac,char *av[]){
struct sockaddr_in servadd; //the number to call
struct hostent *hp; //used to get number
int sock_id,sock_fd; //the socket and fd
char message[BUFSIZ]; //to receive message
int messlen; //for message length

//step1: Get a socket
sock_id = socket(AF_INET, SOCK_STREAM,0);
if(sock_id == -1)
oops("socket");

//step2:connect to server
bzero(&servadd, sizeof(servadd)); //zero the address
hp = gethostbyname(av[1]); //lookup hosts ip
if(hp==NULL)
oops(av[1]);
bcopy(hp->h_addr,(struct sockaddr*)&servadd.sin_addr,hp->h_length);
servadd.sin_port = htons(atoi(av[2]));
servadd.sin_family = AF_INET;

if(connect(sock_id,(struct sockaddr*)&servadd,sizeof(servadd))!=0)
oops("connect");

//step3: transfet data from server, then hangup
messlen = read(sock_id,message,BUFSIZ);
if(messlen == -1)
oops("read");
if(write(1,message,messlen)!=messlen)
oops("write");
close(sock_id);
}

程序流程:

步骤1:向内核请求建立socket

  • 客户端需要一个socket跟网络相连。客户端必须建立Internet域(AF_INET)socket,并且它还必须是SOCK_STREAM

步骤2:与服务器相连

1
2
3
4
5
6
7
8
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
//sockfd 用于建立链接的socket
//addr 指向服务器地址结构的指针
//addrlen 结构的长度
  • 客户端需要连接到时间服务器
  • connect调用试图把由sockfd所标识的socket和由addr所指向的socket地址相连接。如果连接成功,connect返回0。
  • 此时,sockfd是一个合法的文件描述符,可以用来进行读写操作。
  • 写入该文件描述符的数据被发送到连接的另一端的socket,而从另一端写入的数据将从该文件描述符读取

步骤3和4:传送数据和挂断

  • 成功连接之后,进程可以从该文件描述符读写数据,就像与普通的文件或管道相连接一样。在我们的程序中。timeclient只是从服务器读取一行数据
  • 读取时间之后,客户端关闭文件描述符然后退出。若客户端退出而不关闭描述符,内核将完成关闭文件描述符的任务

测试timeclient.c

1
2
3
4
5
[zhangqi@localhost chap11]$ ./timeserv&
[1] 123362
[zhangqi@localhost chap11]$ ./timeclient localhost 13000
Got a call!
The time here is ..Sat Sep 17 12:30:22 2022

另一种服务器:远程的ls

rls(remote ls)可以远程查看另一台机器上的文件列表

需要3个要素来实现rls系统:

  • 协议
  • 客户端程序
  • 服务端程序

协议包含有请求和应答。首先,客户端发送一行包含有目录名称的请求。服务器读取该目录名后打开并读取该目录,然后把文件列表发送到客户端。客户端循环地读取文件列表,直到服务器挂断连接产生文件结尾标志

客户端程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>

#define oops(msg) {perror(msg);exit(1);}
#define PORTNUM 15000

main(int ac,char *av[]){
struct sockaddr_in servadd; //the number to call
struct hostent *hp; //used to get number
int sock_id, sock_fd; //the socket and fd
char buffer[BUFSIZ]; //to receive message
int n_read; //message length

if(ac!=3) exit(1);

//step1: get a socket
sock_id = socket(AF_INET, SOCK_STREAM, 0);
//printf("sock_id:%d\n ",sock_id);
if(sock_id == -1)
oops("socket");

//step2:connect to server
bzero(&servadd, sizeof(servadd));
hp = gethostbyname(av[1]);
if(hp == NULL)
oops(av[1]);

bcopy(hp->h_addr,(struct sockaddr *)&servadd.sin_addr,hp->h_length);
servadd.sin_port = htons(PORTNUM);
servadd.sin_family = AF_INET;

if(connect(sock_id,(struct sockaddr *)&servadd,sizeof(servadd))!=0)
oops("connect");

//step3: send directory name, then read back results
if(write(sock_id, av[2],strlen(av[2]))==-1)
oops("write");
if(write(sock_id,"\n",1)==-1)
oops("write");

while((n_read = read(sock_id,buffer,BUFSIZ))>0)
if(write(1,buffer,n_read)==-1)
oops("write");
close(sock_id);
}

注意该客户端与时间服务客户端的不同之处:

  • rls客户端首先把目录名写到socket中。上面的协议规定了客户端每次发送一行,因此程序中在行尾增加一个换行符。接下来,客户端进入一个循环,将从socket所接收的数据复制到标准输出,直到接收到文件结尾标志。
  • rls.c使用低级别的write和read调用来和服务器交换数据。循环中用到标准大小的缓存以提高效率

服务器程序rlsd

服务器必须得到一个socket,然后调用bind、listen命令,最后调用acceptt来接受一次呼叫。在接收呼叫之后,服务器从socket读取目录名,然后列出该目录下的内容。然后我们可以使用popen读取常规版本的ls程序的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <time.h>
#include <stdbool.h>
#include <ctype.h>

#define PORTNUM 15000

extern void sanitize(char *);

int main(void) {
int sock_id = socket(PF_INET, SOCK_STREAM, 0);

char hostname[BUFSIZ];
gethostname(hostname, BUFSIZ);
struct hostent *hp = gethostbyname(hostname);

struct sockaddr_in saddr;
memset((void *)&saddr, 0, sizeof(saddr));
memcpy((void *)&saddr.sin_addr, (const void *)hp->h_addr_list[0], sizeof(hp->h_length));
saddr.sin_port = PORTNUM;
saddr.sin_family = AF_INET;

bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr));
listen(sock_id, 1);

int sock_fd;
FILE *sock_fpi, *sock_fpo, *pipe_fp;
char dirname[BUFSIZ];
char command[BUFSIZ];
int c;
while (true) {
sock_fd = accept(sock_id, NULL, NULL);
sock_fpi = fdopen(sock_fd, "r");
fgets(dirname, BUFSIZ, sock_fpi);
sanitize(dirname);
sock_fpo = fdopen(sock_fd, "w");
sprintf(command, "ls %s", dirname);
pipe_fp = popen(command, "r");
while ((c = getc(pipe_fp)) != EOF)
putc(c, sock_fpo);
pclose(pipe_fp);
fclose(sock_fpi);
fclose(sock_fpo);
}

close(sock_id);
close(sock_fd);
return EXIT_SUCCESS;
}

extern void sanitize(char *str) {
char *src, *dest;

for (src = dest = str; *src; src++)
if (*src == '/' || isalnum(*src))
*dest++ = *src;

*dest = '\0';
}

注意服务器程序使用标准缓存流来读写数据。服务器用fgets调用从客户端读取目录名。

在调用popen后,服务器就像复制文件一样地使用getc和putc来传输数据。当然,服务器实际上是从本机上的进程向另一台机器上的进程复制数据

注意sanitize函数的使用,对于任何运行参数中所含的命令或从因特网上获取数据的服务器,在编写的时候都要格外小心。程序中的服务器等待接收来自客户端的目录名,然后把它追加到ls命令的尾部。

例如,如果客户端发送字符串 /bin,服务器将创建并运行 ls /bin这是正确的,但是如果有人发送字符串 ; rm *给服务器,服务器将创建并运行 ls ; rm *命令,这会造成被破坏的风险。

连接和协议:编写Web服务器

客户端与服务器的交互主要包含:

  • 服务器设立服务
  • 客户连接到服务器
  • 服务器和客户处理事务

操作1:建立服务器端socket

设立一个服务一般需要以下三步:

  • 创建一个socket

    socket = socket(PF_INET, SOCK_STREAM, 0);

  • 给socket绑定一个地址

    bind(sock,&addr,sizeof(addr));

  • 监听接入请求

    listen(sock,queue_size);

为了避免在编写服务器时重复输入上述代码,可以将三个步骤组合成一个函数:make_server_socket。在编写服务器的时候,只要调用该函数就可以创建一个服务器端的socket

操作2:建立到服务器的连接

基于流的网络客户连接到服务器包含以下两个步骤:

  • 创建一个socket

  • 使用该socket连接到服务器

    connect(sock,&serv_addr,sizeof(serv_addr))

这两个步骤可以抽象成一个函数:connect_to_server。在编写客户端程序时,只要调用该函数就可以建立到服务器的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//socklib.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <time.h>
#include <strings.h>

#define HOSTLEN 256
#define BACKLOG 1

int make_server_socket_q(int,int);
int make_server_socket(int portnum){
return make_server_socket_q(portnum,BACKLOG);
}
int make server_socket_q(int portnum, int backlog){
struct sockaddr_in saddr;
struct hostent *hp;
char hostname[HOSTLEN];
int sock_id;

sock_id = socket(PF_INET,SOCK_STREAM,0);
if(sock_id == -1)
return -1;

bzero((void*)&saddr,sizeof(saddr));
gethostname(hostname,HOSTLEN);
hp = gethostbyname(hostname);

bcopy((void *)hp->h_addr,(void*)&saddr.sin_addr,hp->h_length);
saddr.sin_port = htons(portnum);
saddr.sin_family = AF_INET;
if(bind(sock_id,(struct sockaddr*)&saddr,sizeof(saddr))!=0)
return -1;

if(listen(sock_id,backlog) != 0)
return -1;

return sock_id;
}

int connect_to_server(char* host, int portnum){
int sock;
struct sockaddr_in servadd;
struct hostent *hp;

sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
return -1;

bzero((void*)&servadd,sizeof(servadd));
hp = gethostbyname(host);
if(hp == NULL)
return -1;
bcopy(hp->h_addr,(struct sockaddr *)&servadd.sin_addr, hp->h_length);
servadd.sin_port = htons(portnum);
servadd.sin_family = AF_INET;

if(connect(sock,(struct sockaddr *)&servadd,sizeof(servadd))!=0)
return -1;

return sock;
}

操作3:客户/服务器的会话

一个典型的客户程序:

1
2
3
4
5
6
7
main(){
int fd;
fd = connect_to_server(host,port);
if(fd == -1) exit(1);
talk_with_server(fd);//根据不同的应用处理与服务器的会话
close(fd);
}

典型的服务器端:

1
2
3
4
5
6
7
8
9
10
11
main(){
int sock,fd;
sock = make_server_socket(port);
if(sock == -1) exit(1);
while(1){
fd = accept(sock,NULL,NULL);
if(fd == -1) break;
process_request(fd);//处理客户的请求
close(fd);
}
}

使用socklib.c的timeserv和timeclient

只要编写处理回话的talk_with_server用于客户端,process_request用于服务器端就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
talk_with_server(fd){
char buf[LEN];
int n;

n = read(fd,buf,LEN);
write(1,buf,n);
}

process_request(fd){
time_t now;
char *cp;
time(&now);
cp = ctime(&now);
write(fd,cp,strlen(cp));
}

第二版的服务器:使用fork

不通过调用time函数来获得时间,而是直接使用一个shell命令(date)

服务器用fork建立一个新的子进程,该子进程将标准输出重定向到socket,然后运行date。date命令给出日期,然后将日期写到标准输出,这样就把字符串发送到客户端了。

代码:

1
2
3
4
5
6
7
8
9
10
11
process_request(fd){
int pid = fork();
switch(pid){
case -1:return;
case 0:dup2(fd,1);
close(fd);
execl("/bin/date","date",NULL);
oops("execlp");
default:wait(NULL);
}
}

服务器的设计问题:DIY或代理

两种设计方法:

  • 自己做(Do It Yourself,DIY)——服务器接收请求,自己处理工作
    • 适用于快速简单的任务。
    • 不需要额外的系统调用(fork()、exec() ……)和创建新的进程的开销。
    • 对于一些服务器,效率最高的方法是服务器自己来完成工作并且在listen中限制连接队列的大小。
  • 代理——服务器接收请求,然后创建一个新进程来处理工作
    • 适用于慢速的更加复杂的任务。
    • 可以使服务器同时处理多个任务

使用SIGCHLD来阻止僵尸进程问题

  • 除了等待子进程死亡外,父进程可以设置为接收表示子进程死亡的信号(为SIGCHLD设置一个信号处理函数,它可以调用wait)

  • 具体方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //SIGCHLD   20,17,18    Ign     Child stopped or terminated
    main(){
    int sock,fd;
    void child_waiter(int), process_request(int);
    signal(SIGCHLD, child_waiter);
    if((sock = make_server_socket(PORTNUM)) == -1)
    oops("make_server_socket");;
    while(1){
    fd = accept(sock,NULL,NULL);
    if(fd == -1) break;
    process_request(fd);
    close(fd);
    }
    }
    void child_waiter(int signum){
    wait(NULL);
    }
    void process_request(int fd){
    if(fork() == 0){
    dup2(fd,1);
    close(fd);
    execlp("data","date",NULL);
    oops("execlp date");
    }
    }
  • 该程序的流程控制:当收到一个请求,父进程使用fork,然后父进程立即返回去接收下一个请求,调到处理函数并调用wait。子进程从进程表中被删除,父进程从处理函数返回到主函数。

  • 该过程存在两个问题:

    • 程序运行到信号处理函数跳转时会终端系统调用accept。accpet被信号中断时会返回-1,然后设置errno到EINTR。代码中把accept返回的-1作为错误,然后从主循环中跳出。因此需要更改main函数来区分真正的错误和被打断的系统调用产生的错误

    • Unix是如何处理多个信号的,多个子进程几乎同时退出,将会发生什么?假设同时有3个SIGCHLD大送到父进程,最先到达的信号导致父进程调到处理函数,此时其他两个信号的到达导致Unix阻塞,但是并不缓存信号。从而,第二个信号被阻塞,而第三个信号丢失了。此时如果还有其他的子进程退出,这些信号也将丢失。会产生很多僵尸进程。

      解决方法是在处理函数中调用wait足够多的次数来去除所有的终止进程:

      1
      2
      3
      4
      5
      //waitpid函数解决了这个问题
      void child_waiter(int signum){
      //该循环直到所有推出的子进程都被等待了才停止,即所有信号都会被处理
      while(waitpid(-1,NULL,WHOHANG)>0);
      }

编写Web服务器

功能:

  • 列举目录信息
  • cat文件
  • 运行程序

Web服务器通过基于流的socket连接为客户提供上述三种操作。

如图所示,用户连接到服务器后,发送请求,然后服务器返回客户请求的信息

设计Web服务器

要编写的操作如下:

  1. 建立服务器。使用(make_server_socket)
  2. 接收请求。使用accept来得到指向客户端的文件描述符,使用fdopen使得该文件描述符转换成缓冲流
  3. 读取请求
  4. 处理请求。
  5. 发送应答

Web服务器协议

请求和应答的格式在超文本传输协议(HTTP)中有定义。

可以用telnet和Web服务器交互。Web服务器在端口80监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[zhangqi@localhost chap11]$ telnet
telnet> o example.com 80
Trying 93.184.216.34...
Connected to example.com.
Escape character is '^]'.
HEAD /index.html HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Accept-Ranges: bytes
Age: 555320
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 17 Sep 2022 13:44:31 GMT
Etag: "3147526947+gzip"
Expires: Sat, 24 Sep 2022 13:44:31 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (sab/56BC)
X-Cache: HIT
Content-Length: 1256

GET方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[zhangqi@localhost chap11]$ telnet
telnet> o example.com 80
Trying 93.184.216.34...
Connected to example.com.
Escape character is '^]'.
GET /index.html HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Accept-Ranges: bytes
Age: 555530
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 17 Sep 2022 13:48:01 GMT
Etag: "3147526947+gzip"
Expires: Sat, 24 Sep 2022 13:48:01 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (sab/56BC)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256

<!doctype html>
<html>
<head>
<title>Example Domain</title>

<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>

<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
  1. HTTP请求:GET

    telnet创建了一个socket并调用了connect来连接到Web服务器。服务器接收连接请求,并创建了一个基于socket的从客户端的键盘到Web服务进程的数据通道。

    添加了Host的可选参数

  2. HTTP应答:OK

    • 服务器读取请求,检查请求,然后返回一个请求。应答有两部分:头部和内容。头部以状态行起始:

      HTTP/1.1 200 OK

    • 状态行含有两个或更多个字符串。第一个串是协议的版本,第二个串是返回码,这里是200,其文本解释是OK。如果服务器中没有所请求的文件名,返回码将是404,解释是“未找到”

    • 头部的其余部分是关于应答的附加信息,在该例子中,包含服务器名、应答时间、服务器所发送数据类型以及应答的连接类型。一个应答头部可以包含多行信息,以空行表示结束。

    • 应答的其余部分是返回的具体内容,这里,服务器返回了文件/index.html的内容

编写Web服务器

要求Web服务器只支持GET命令,只接收请求行,跳过其余参数,然后处理请求和发送应答。

主要循环:

1
2
3
4
5
6
7
8
while(1){
fd = accept(sock,NULL,NULL); //take a call
fpin = fdopen(fd, "r"); //make it a FILE*
fgets(fpin, request, LEN); //read client request
read_until_crnl(fpin); //skip over arguments
process_rq(request,fd); //reply to client
fclose(fpin); //hang up connection
}

简介起见,这里忽略了出错检查

  1. 处理请求:包含识别命令和根据参数进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    process_rq(char * rq, int fd){
    char cmd[11],arg[513];

    if(fork()!=0)
    return;
    sscanf(rq,"%10s %512s",cmd, arg);
    if(strcmp(cmd,"GET")!=0)
    connot_do(fd);
    else if(not_exist(arg))
    do_404(arg,fd);
    else if(isadir(fd))
    do_ls(arg,fd);
    else if(ends_in_cgi(arg))
    do_exec(arg,fd);
    else
    do_cat(arg,fd);
    }

    服务器为每个请求创建一个新的进程来处理。子进程将请求分割成命令和参数。如果命令不是GET,服务器应答HTTP返回码表示未实现的命令。如果命令是GET,服务器将期望得到目录名,一个以 .cgi结尾的可执行程序或文件名。如果没有该目录或指定文件名,则服务器报错。

    如果存在目录或文件,服务器决定所要使用的操作:ls、exec或cat

  2. 目录列表函数

    函数do_ls处理列出目录信息的请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    do_ls(char * dir, int fd){
    FILE * fp;

    fp = fdopen(fd,"w");
    header(fp,"text/plain");
    fprintf(fp,"\r\n");
    fflush(fp);

    dup2(fd,1);
    dup2(fd,2);
    close(fd);
    execl("/bin/ls","ls","-l",dir,NULL);
    perror(dir);
    exit(1);
    }
  3. 其他函数

    其他函数包含在本章后面部分中。程序可以工作,但它并不完整,也不安全。需要做如下改进:

    • 僵尸进程的去除
    • 缓存溢出保护
    • CGI(Common Gateway Interface,通用网关接口)程序需要设置一些环境变量
    • HTTP头部可以包含更多的信息

webserv的源程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <string.h>

extern int make_server_socket(int port);
extern int make_server_socket_q(int port, int backlog);

extern void process_rq(char *request, int sock_fd);
extern void header(FILE *sock_fp, char *content_type);
extern void cannot_do(int sock_fd);
extern int not_exist(char *f);
extern void do_404(char *item, int sock_fd);
extern int is_dir(char *dir);
extern void do_ls(char *arg, int sock_fd);
extern char *file_type(char *f);
extern int ends_in_cgi(char *arg);
extern void do_exec(char *comm, int sock_fd);
extern void do_cat(char *f, int sock_fd);

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: ./webserv portnum\n");
exit(EXIT_FAILURE);
}

int sock_id = make_server_socket(atoi(argv[1]));

int sock_fd;
char request[BUFSIZ];
FILE *sock_fp;
while (true) { // 循环中自然可以多次接收命令
sock_fd = accept(sock_id, NULL, NULL);
sock_fp = fdopen(sock_fd, "r");
fgets(request, BUFSIZ, sock_fp);
printf("got a call: request = %s\n", request);
// sock_fp 和 sock_fd 的位置就是同一个,所以必须处理掉 sock_fp 的输出
char buf[BUFSIZ];
while (fgets(buf, BUFSIZ, sock_fp) != NULL && strcmp(buf, "\r\n") != 0)
;

process_rq(request, sock_fd);
fclose(sock_fp);
}
return EXIT_SUCCESS;
}

//rq is HTTP command: GET /foo/bar.html HTTP/1.0
extern void process_rq(char *request, int sock_fd) {
char cmd[BUFSIZ], arg[BUFSIZ];
strcpy(arg, "./");
sscanf(request, "%s %s", cmd, arg + 2);

if (fork() != 0)
return ;

if (strcmp(cmd, "GET") != 0) {
cannot_do(sock_fd);
} else if (not_exist(arg)) {
do_404(arg, sock_fd);
} else if (is_dir(arg)) {
do_ls(arg, sock_fd);
} else if (ends_in_cgi(arg)) {
do_exec(arg, sock_fd);
} else {
do_cat(arg, sock_fd);
}
}

extern void header(FILE *sock_fp, char *content_type) {
fprintf(sock_fp, "HTTP/1.0 200 OK\r\n");
if (content_type)
fprintf(sock_fp, "Content-type: %s\r\n", content_type);
}

extern void cannot_do(int sock_fd) {
FILE *sock_fp = fdopen(sock_fd, "w");
fprintf(sock_fp, "HTTP/1.0 501 Not Implemented\r\n"
"Content-type: text/plain\r\n"
"\r\n"
"That command is not yet implementd\r\n");
fclose(sock_fp);
}

extern int not_exist(char *f) {
struct stat info;
return (stat(f, &info) == -1);
}

extern void do_404(char *item, int sock_fd) {
FILE *sock_fp = fdopen(sock_fd, "w");
fprintf(sock_fp, "HTTP/1.0 404 Not Found\r\n"
"Content-type: text/plain\r\n"
"\r\n"
"The item you requested: %s\r\n"
"is not found\r\n", item);
fclose(sock_fp);
}

extern int is_dir(char *dir) {
struct stat info;
return (stat(dir, &info) != -1 && S_ISDIR(info.st_mode));
}

extern void do_ls(char *dir, int sock_fd) {
FILE *sock_fp = fdopen(sock_fd, "w");
header(sock_fp, "text/plain");
fprintf(sock_fp, "\r\n");
fflush(sock_fp);

dup2(sock_fd, 1);
dup2(sock_fd, 2);
close(sock_fd);

execlp("/bin/ls", "ls", dir, NULL);
perror(dir);
exit(EXIT_FAILURE);
}

extern char *file_type(char *f) {
char *cp;
if ((cp = strrchr(f, '.')) != NULL)
return cp + 1;
return "";
}

extern int ends_in_cgi(char *f) {
return (strcmp(file_type(f), "cgi") == 0);
}

extern void do_exec(char *comm, int sock_fd) {
FILE *sock_fp = fdopen(sock_fd, "w");
header(sock_fp, NULL);
fflush(sock_fp);

dup2(sock_fd, 1);
dup2(sock_fd, 2);
close(sock_fd);
execl(comm, comm, NULL);
perror(comm);
// 这里没有 exit(), 服务器可以继续接收命令
}

extern void do_cat(char *f, int sock_fd) {
char *extension = file_type(f);
char *content = "text/plain";

if (strcmp(extension, "html") == 0)
content = "text/html";
else if (strcmp(extension, "gif") == 0)
content = "image/gif";
else if (strcmp(extension, "jpg") == 0)
content = "image/jpg";
else if (strcmp(extension, "jpeg") == 0)
content = "image/jpeg";

FILE *sock_fp = fdopen(sock_fd, "w");
FILE *file_fp = fopen(f, "r");
int c;
if (sock_fp != NULL && file_fp != NULL) {
header(sock_fp, NULL);
fprintf(sock_fp, "\r\n");
while ((c = getc(file_fp)) != EOF)
putc(c, sock_fp);
fclose(sock_fp);
fclose(file_fp);
}
exit(EXIT_SUCCESS);
}

编译并运行

1
2
3
4
5
6
7
8
9
[zhangqi@localhost chap11]$ ./webserver 12345

#在浏览器输入localhost:12345
got a call: request = GET / HTTP/1.1

got a call: request = GET /favicon.ico HTTP/1.1

#在浏览器输入localhost:12345/hello.cgi
got a call: request = GET /hello.cgi HTTP/1.1

将html文件放到该目录中,并且用localhost:12345//filename.html来打开它

创建下面的shell脚本:

1
2
3
#! /bin/sh
# hello.cgi-a cheery cgi page
printf "Content-type:text/plain\n\nhello\n"

命名为hello.cgi,用chmod改变权限位755,然后用浏览器调用改程序:

http://localhost:12345/hello.cgi

基于数据包(Datagram)的编程

线程机制:并发函数的使用

考虑一个程序如何才能在同一时刻完成多个任务?

前面曾使用fork和exec同时运行多个程序,但如果希望同时运行几个函数,或同时对一个函数调用很多次要怎么做?

因此引入线程的概念。线程相对于函数类似于进程相对于程序,后者为前者提供了运行环境。很多函数可以同时运行,但它们都在相同的进程中。

函数的执行路线

单线程程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

#define NUM 5

main(){
void print_msg(char *);

print_msg("hello");
print_msg("world\n");
}

void print_msg(char *m){
int i;
for(i=0;i<NUM;++i){
printf("%s",m);
fflush(stdout);
sleep(1);
}
}

编译并运行:可以看到main函数顺序地调用了两个函数。每个函数执行了一个循环。

1
2
3
4
5
6
[zhangqi@localhost chap14]$ ./hello_single 
hellohellohellohellohelloworld
world
world
world
world

一个多线程程序

如果想同时执行两个对于print_msg函数的调用,它必须创建多个线程:

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <pthread.h>

#define NUM 5

main(){
pthread_t t1,t2; //two threads

void* print_msg(void *);

pthread_create(&t1, NULL, print_msg,(void*)"hello");//对应一个控制流分支
pthread_create(&t2, NULL, print_msg,(void*)"world\n");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
}

void* print_msg(void *m){
int i;
for(i=0;i<NUM;++i){
printf("%s",m);
fflush(stdout);
sleep(1);
}
return NULL;
}

编译并运行

1
2
3
4
5
6
7
8
9
10
[zhangqi@localhost chap14]$ cc hello_multi.c -lpthread -o  hello_multi
[zhangqi@localhost chap14]$ ls
hello_multi hello_multi.c hello_single hello_single.c
[zhangqi@localhost chap14]$ ./hello_multi
helloworld
helloworld
helloworld
helloworld
helloworld
#注意这里的输出根据线程调度的不同可能导致不一样的结果

相关调用:

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//thread: 指向pthread_t类型变量的指针
//attr: 指向pthread_attr_t类型变量的指针,或者为NULL
//func: 指向新线程所运行函数的指针
//arg: 传递给func的参数

//成功,返回0,失败,errcode

pthread_create函数创建了一条新的执行路线,在此新的线程内调用了func(arg)。新线程的属性由attr参数来指定,func是一个函数,它接收一个指针作为它的参数,并且运行结束后返回一个指针。

参数和返回值都被定义为类型为void*的指针,以允许它们指向任何类型的值。

1
2
3
4
5
6
7
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
//thread: 所等待的线程
//retval: 指向某存储线程返回值的变量

//成功,返回0,失败,errcode

pthread_join使得调用线程挂起直至由thread参数指定的线程终止。如果retval不是null,那么线程的返回值就存储在由retval指向的变量中。

错误发生的情况:试图等待一个不存在的线程、多个线程同时等待一个线程返回、线程试图等待自己

线程使用示例

进程间可以通过管道、socket、信号、退出/等待以及运行环境来进行会话。

线程间通信也很容易,由于多个线程在一个单独的进程中运行,共享全局变量,因此线程间可以通过设置和读取这些全局变量来进行通信。不过对共享内存的访问是既有用又非常危险的,要非常小心。

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//incrprint.c
#include <stdio.h>
#include <pthread.h>

#define NUM 5

int counter = 0;
main(){
pthread_t t1;
void *print_count(void*);
int i;

pthread_create(&t1, NULL, print_count, NULL);
for(i = 0; i < NUM; ++i){
++counter;
sleep(1);
}
pthread_join(t1,NULL);
}

void *print_count(void *m){
int i;
for(i = 0; i < NUM; ++i){
printf("count = %d\n",counter);
sleep(1);
}
return NULL;
}

该程序使用了两个线程。初始线程执行了一个循环来使计数器值每秒钟增1。初始线程在进入循环之前,创建了一个新的线程。

新的线程运行了一个函数来讲counter的值打印出来。

由于main函数和print_count函数运行在同一个进程中,所以都有对于counter的访问权。当main函数改变了counter值之后,print_counter函数立即可以访问到新的值。因此不需要通过管道或套接字等方法传送新的值。

编译并运行:可以看到一个函数修改了变量,另一个函数读取并显示了变量的值

1
2
3
4
5
6
[zhangqi@localhost chap14]$ ./incrprint 
count = 1
count = 2
count = 3
count = 4
count = 5

示例2:twordcount.c

设计一个统计多个文件中总字数的多线程程序(用并行的思想,分别分给每个线程一个文件,然后将结果累加)

Unix平台的wc程序可以计算一个或多个文件中的行、单词以及字符个数。但是wc是单线程程序。

版本1:两个线程,一个计数器

  • 程序创建分开的线程来对每一个文件进行计算。所有的线程在检查到单词的时候对同一个计数器增值

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//twordcount1.c
#include <stdio.h>
#include <pthread.h>
#include <ctype.h>

int total_words;

main(int ac, char *av[]){
pthread_t t1,t2;
void *count_words(void *);

if(ac!=3){
printf("usage: %s file1 file2\n",av[0]);
exit(1);
}
total_words = 0;
pthread_create(&t1, NULL, count_words,(void*)av[1]);
pthread_create(&t2, NULL, count_words,(void*)av[2]);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%5d:total words\n", total_words);
}

void* count_words(void* f){
char * filename = (char*)f;
FILE* fp;

int c, prevc = '\0';

if((fp = fopen(filename,"r"))!=NULL){
while((c = getc(fp))!=EOF){
if(!isalnum(c) && isalnum(prevc))
++total_words;
prevc = c;
}
fclose(fp);
}
else
perror(filename);

return NULL;
}

编译并运行

1
2
3
[zhangqi@localhost chap14]$ cc twordcount1.c -lpthread -o twordcount1
[zhangqi@localhost chap14]$ ./twordcount1 /etc/group /bin/stat
12723:total words

此程序存在问题:如果所有线程对同一个计数器进行操作,并且同时进行,会出现问题。

由于total_words++并不是一个原子操作,计算机可能先将计数器当前值存入寄存器中,加1操作后,再恢复到内存中。

这样如果所有线程在同一时刻都使用 ”取出 - 》加1存储“ 的序列来完成对计数器的操作,会产生线程之间的干扰现象。

版本2:两个线程、一个计数器、一个互斥量

线程系统包含了称为互斥锁的变量,它可以使线程间很好的合作,避免对于变量、函数以及资源的访问冲突

示例程序:(改进上一部分的代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//twordcount2.c
//实际只增添了三行代码:定义一个pthread_mutex_t类型的全局变量counter_lock,赋一个初值
//在count_words++的前后分别调用pthread_mutex_lock以及pthread_mutex_unlock
#include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>

int total_words;
pthread_mutex_t counter_lock = PTHREAD_MUTEX_INITIALIZER;

main(int ac, char *av[]){
pthread_t t1,t2;
void *count_words(void *);

if(ac!=3){
printf("usage: %s file1 file2\n",av[0]);
exit(1);
}
total_words = 0;
pthread_create(&t1, NULL, count_words,(void*)av[1]);
pthread_create(&t2, NULL, count_words,(void*)av[2]);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%5d:total words\n", total_words);
}

void* count_words(void* f){
char * filename = (char*)f;
FILE* fp;

int c, prevc = '\0';

if((fp = fopen(filename,"r"))!=NULL){
while((c = getc(fp))!=EOF){
if(!isalnum(c) && isalnum(prevc)){
pthread_mutex_lock(&counter_lock);
++total_words;
pthread_mutex_unlock(&counter_lock);
}
prevc = c;
}
fclose(fp);
}
else
perror(filename);

return NULL;
}

这样两个线程可以安全地共享计数器了。当一个线程调用 pthread_mutex_lock的时候,如果另一个线程已经将这个互斥量锁住了,那这个线程只好阻塞等待着这个锁被另一个线程解开后,才可以对计数器进行操作。每个线程对计数器进行操作后,都将互斥量解锁,然后循环地处理其他数据

任何数目的线程都可以挂起等待互斥量解锁。当一个线程对互斥量解锁之后,系统就将控制权教给等候的某一线程。

1
2
3
4
5
6
7
8
9
10
11
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
//用于锁住指定的互斥量。如果互斥量是开放的,它被锁住,并只能由调用线程来管理
//mutex:指向互斥锁对象的指针
//成功,返回0,失败,返回errcode

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);
//给指定的互斥量解锁。如果有线程挂起等待此互斥量,其中一个线程将获得对互斥锁的控制权

使用互斥量会使程序运行速度变慢。对所有文件中的每一个单词都需要执行检查,设置以及释放所的操作,这使得程序效率低下。

更有效的方法是为每个线程设置自己的计数器

版本3:两个线程、两个计数器、向线程传递多个参数

下面这个程序避免了对互斥量的使用。当线程返回时,再将这两个计数器的值加起来得到最后的结果

线程可以通过调用pthread_exit来得到返回值,这个返回值又可以通过pthread_join的调用被原先的线程得到。

不过这里使用一个简单点的方法:向函数传递一个指向一个变量的指针,让函数对变量进行操作,从而避免让线程将值返回。但是pthread_create只能允许传递一个参数给函数,因此我们要建一个包含两个成员的结构体,然后将此结构体的地址传给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//twordcount3.c
include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>

struct arg_set{
char * fname;
int count;
};
main(int ac, char *av[]){
pthread_t t1,t2;
struct arg_set args1, args2;
void *count_words(void *);

if(ac!=3){
printf("usage: %s file1 file2\n",av[0]);
exit(1);
}

args1.fname = av[1];
args1.count = 0;
args2.fname = av[2];
args2.count = 0;

pthread_create(&t1, NULL, count_words,(void*)&args1);
pthread_create(&t2, NULL, count_words,(void*)&args2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%5d: %s\n", args1.count,av[1]);
printf("%5d: %s\n", args2.count,av[2]);
printf("%5d: total words\n",args1.count+args2.count);
}

void* count_words(void* a){
struct arg_set *args = a;
char * filename = args->fname;
FILE* fp;

int c, prevc = '\0';

if((fp = fopen(filename,"r"))!=NULL){
while((c = getc(fp))!=EOF){
if(!isalnum(c) && isalnum(prevc))
++(args->count);
prevc = c;
}
fclose(fp);
}
else
perror(filename);

return NULL;
}

线程与进程

进程与线程有根本上的不同:

  • 每个进程有其独立的数据空间、文件描述符以及进程ID
  • 线程共享一个数据空间、文件描述符以及进程ID

共享数据空间:

  • 考虑一个在存储器中村塾了巨大而复杂的树结构数据库的数据库系统。多个线程可以轻易读到这个共享的数据集。客户的多个查询可以由一个进程来实现。如果变量不会被改变,共享这个数据空间不会导致任何问题
  • 考虑一个使用malloc和free系统调用来管理内存的程序。一个线程分配了一块空间存储一个字符串。当此线程做其他事情的时候,另一个线程使用free释放了这块空间。那么原先的线程中本来指向此空间的指针现在指向了一块已经被释放的地方(甚至这块内存已经被别的地方使用了),这会导致严重的错误
  • 线程机制还会带来内存的堆积。因为程序员往往害怕影响者在使用的内存空间,只分配而不释放存储区域。这直接导致了内存的囤积,使用完毕也得不到释放
  • 单线程环境中返回指向静态局部变量的指针的函数无法兼容与多线程环境。因为同样的函数可能在多个线程中同时被调用而导致结果出错。

共享的文件描述符:

  • fork原语调用之后,文件描述符自动地被赋值,从而子进程得到了一套新的文件描述符。在子进程关闭了某一个从父进程哪里继承来的文件描述符之后,此描述符对父进程来说仍然是打开的。
  • 多线程程序中,很可能会将同一个文件描述符传递给两个不同的线程,即传递给它们的两个值指向同一个文件描述符。显然如果一个线程中的函数关闭了这个文件,此文件描述符对此进程中的任何线程来说都已经被关闭。然而其他县城或许仍然需要对此文件描述符的连接。

fork、exec、exit、signals

  • 所有线程共享同一个进程。如果一个线程调用了exec,系统内核用一个新的程序取代当前的程序,从而所有正在运行的线程都会消失。如果一个线程执行了exit,那么整个进程都将结束
  • 如果线程中的某函数调用了fork,其他的线程不会被复制给新的进程,只有调用fork的线程在新的进程中运行。
  • 进程可以接收任何种类的信号量

线程间通信

对于进程来说,当子进程终止后,系统调用wait返回。

线程没有类似的机制,因为对于线程而言没有父子的概念,不存在某一个明显的线程可以去通知

使用条件变量编写程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>

struct arg_set{
char * fname;
int count;
};

struct arg_set * mailbox; //保存数据
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //互斥量
pthread_cond_t flag = PTHREAD_MUTEX_INITIALIZER; //条件变量

main(int ac, char *av[]){
pthread_t t1,t2;
struct arg_set args1, args2;
void *count_words(void *);

int reports_in = 0;
int total_words = 0;

if(ac!=3){
printf("usage: %s file1 file2\n",av[0]);
exit(1);
}
pthread_mutex_lock(&lock);

args1.fname = av[1];
args1.count = 0;
args2.fname = av[2];
args2.count = 0;

pthread_create(&t1, NULL, count_words,(void*)&args1);
pthread_create(&t2, NULL, count_words,(void*)&args2);

while(reports_in<2){//两次处理mailbox中的信息
printf("MAIN: waiting for flag to go up\n");
pthread_cond_wait(&flag,&lock);
printf("MAIN: Wow!flag was raised, I have the lock\n");
printf("%7d: %s\n",mailbox->count,mailbox->fname);
total_words += mailbox->count;
if(mailbox == &args1)
pthread_join(t1,NULL);
if(mailbox == &args2)
pthread_join(t2,NULL);
mailbox = NULL;
pthread_cond_signal(&flag);
reports_in++;
}
printf("%7d: total words\n",total_words);
}

void* count_words(void* a){
struct arg_set *args = a;
char * filename = args->fname;
FILE* fp;

int c, prevc = '\0';
if((fp = fopen(filename,"r"))!=NULL){
while((c = getc(fp))!=EOF){
if(!isalnum(c) && isalnum(prevc))
++(args->count);
prevc = c;
}
fclose(fp);
}
else
perror(filename);

printf("COUNT: waiting to get lock\n");
pthread_mutex_lock(&lock);
printf("COUNT: have lock, storing data\n");
if(mailbox != NULL)
pthread_cond_wait(&flag, &lock);
mailbox = args;
printf("COUNT: raising flag\n");
pthread_cond_signal(&flag);
printf("COUNT: unlocking box\n");
pthread_mutex_unlock(&lock);
return NULL;
}

编译并运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zhangqi@localhost ~/ZZQ/test/chap14]$ ./twordcount4 /etc/group /bin/stat
MAIN: waiting for flag to go up
COUNT: waiting to get lock
COUNT: have lock, storing data
COUNT: raising flag
COUNT: unlocking box
MAIN: Wow!flag was raised, I have the lock
12494: /bin/stat
MAIN: waiting for flag to go up
COUNT: waiting to get lock
COUNT: have lock, storing data
COUNT: raising flag
COUNT: unlocking box
MAIN: Wow!flag was raised, I have the lock
229: /etc/group
12723: total words

注意线程中调用pthread_cond_wait函数来等待条件变量的改变。

  • 该调用使线程挂起直到另一个线程通过条件变量发出消息。
  • pthread_cond_wait总是和互斥锁在一起使用。
  • 此函数先自动释放指定的锁,然后等待条件变量的变化。如果在调用此函数之前,互斥量mutex并没有被锁住,函数执行的结果是不确定的。
  • 在返回原调用函数之前,此函数自动将指定的互斥量重新锁住
1
2
3
4
5
6
7
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//cond 指向某条件变量的指针
//mutex 指向互斥锁对象的指针
//成功,返回0,错误,errcode

pthread_cond_signal函数通过条件变量cond发消息。若没有线程等候消息,什么都不会发生;若是多个线程都在等待,只唤醒它们中的一个。

1
2
3
4
5
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
//cond 指向某条件变量的指针
//成功,返回0,失败,errcode

个人思考:

  • 在存储消息到共享变量这一步的前后加锁
  • 保证信息存储完成以后进程要完成对信息的处理,没完成处理则等待

多线程Web服务器

更改前一个版本的Web服务器程序,使用pthread_create替换fork,使客户端的请求不再由单独的进程来处理,而是由同一进程的多个线程来处理。

另外之前通过exec来执行ls命令完成对目录的列表,这里重新写了一个对目录进行列表的函数。

多线程的特性允许我们添加一个新的功能:内部统计。服务器的运行者通常希望知道服务器的运行时间、接收的客户端请求的数目以及发送回客户端的数据量。

因为对于所有的请求共享内存空间,可以使用共享变量的方式来进行统计。

用户如何访问这些统计数据呢?加入一个特殊的URL:status。当远程用户请求此URL时,服务器将内部的统计数据发给客户端。

防止僵尸线程:独立线程

注意这里提到的所有程序都使用了pthread_join函数来等待线程的返回。每个线程都占用了系统资源。如果程序员忘记使用pthread_join来收回线程,这些线程所占用的资源就无法被回收。

在字数统计的程序中,原线程不得不等待所有的计数线程返回之后,才可以收集数据。但是Web服务器没有理由等待处理请求的线程返回。因为原线程不需要从这些线程得到任何返回数据。

这里同样可以创建不需要返回的线程,称之为独立线程(Detached Threads)。当函数执行完毕之后,独立线程自动释放它所占用的所有的资源,他们自身甚至也不允许等待其他的线程返回。可以通过传递一个特殊的属性参数给函数pthread_create来创建一个独立线程

1
2
3
4
5
6
//creating a detached thread
pthread_t t;
pthread_attr_t attr_detached;
pthread_attr_init(&attr_detached);
pthread_attr_setdetached(&attr_detached, PTHREAD_CREATE_DETACHED);
pthread_create(&t, &attr_detached,func,arg);

多线程Web服务器完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/* twebserv.c - a threaded minimal web server (version 0.2)
* usage: tws portnumber
* features: supports the GET command only
* runs in the current directory
* creates a thread to handle each request
* supports a special status URL to report internal state
* building: cc twebserv.c socklib.c -lpthread -o twebserv
*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#include <dirent.h>
#include <time.h>

/* server facts here */

time_t server_started ;
int server_bytes_sent;
int server_requests;

main(int ac, char *av[])
{
int sock, fd;
int *fdptr;
pthread_t worker;
pthread_attr_t attr;

void *handle_call(void *);

if ( ac == 1 ){
fprintf(stderr,"usage: tws portnum\n");
exit(1);
}
sock = make_server_socket( atoi(av[1]) );
if ( sock == -1 ) { perror("making socket"); exit(2); }

setup(&attr);

/* main loop here: take call, handle call in new thread */

while(1){
fd = accept( sock, NULL, NULL );
server_requests++;

fdptr = malloc(sizeof(int));
*fdptr = fd;
pthread_create(&worker,&attr,handle_call,fdptr);
}
}

/*
* initialize the status variables and
* set the thread attribute to detached
*/
setup(pthread_attr_t *attrp)
{
pthread_attr_init(attrp);
pthread_attr_setdetachstate(attrp,PTHREAD_CREATE_DETACHED);

time(&server_started);
server_requests = 0;
server_bytes_sent = 0;
}

void *handle_call(void *fdptr)
{
FILE *fpin;
char request[BUFSIZ];
int fd ;

fd = *(int *)fdptr;
free(fdptr); /* get fd from arg */

fpin = fdopen(fd, "r"); /* buffer input */
fgets(request,BUFSIZ,fpin); /* read client request */
printf("got a call on %d: request = %s", fd, request);
skip_rest_of_header(fpin);

process_rq(request, fd); /* process client rq */

fclose(fpin);
}

/* ------------------------------------------------------ *
skip_rest_of_header(FILE *)
skip over all request info until a CRNL is seen
------------------------------------------------------ */
skip_rest_of_header(FILE *fp)
{
char buf[BUFSIZ];
while( fgets(buf,BUFSIZ,fp) != NULL && strcmp(buf,"\r\n") != 0 )
;
}

/* ------------------------------------------------------ *
process_rq( char *rq, int fd )
do what the request asks for and write reply to fd
handles request in a new process
rq is HTTP command: GET /foo/bar.html HTTP/1.0
------------------------------------------------------ */
process_rq( char *rq, int fd)
{
char cmd[BUFSIZ], arg[BUFSIZ];

if ( sscanf(rq, "%s%s", cmd, arg) != 2 )
return;
sanitize(arg);
printf("sanitized version is %s\n", arg);

if ( strcmp(cmd,"GET") != 0 )
not_implemented();
else if ( built_in(arg, fd) )
;
else if ( not_exist( arg ) )
do_404(arg, fd);
else if ( isadir( arg ) )
do_ls( arg, fd );
else
do_cat( arg, fd );
}
/*
* make sure all paths are below the current directory
*/
sanitize(char *str)
{
char *src, *dest;

src = dest = str;

while( *src ){
if( strncmp(src,"/../",4) == 0 )
src += 3;
else if ( strncmp(src,"//",2) == 0 )
src++;
else
*dest++ = *src++;
}
*dest = '\0';
if ( *str == '/' )
strcpy(str,str+1);

if ( str[0]=='\0' || strcmp(str,"./")==0 || strcmp(str,"./..")==0 )
strcpy(str,".");
}

/* handle built-in URLs here. Only one so far is "status" */
built_in(char *arg, int fd)
{
FILE *fp;

if ( strcmp(arg,"status") != 0 )
return 0;
http_reply(fd, &fp, 200, "OK", "text/plain",NULL);

fprintf(fp,"Server started: %s", ctime(&server_started));
fprintf(fp,"Total requests: %d\n", server_requests);
fprintf(fp,"Bytes sent out: %d\n", server_bytes_sent);
fclose(fp);
return 1;
}

http_reply(int fd, FILE **fpp, int code, char *msg, char *type, char *content)
{
FILE *fp = fdopen(fd, "w");
int bytes = 0;

if ( fp != NULL ){
bytes = fprintf(fp,"HTTP/1.0 %d %s\r\n", code, msg);
bytes += fprintf(fp,"Content-type: %s\r\n\r\n", type);
if ( content )
bytes += fprintf(fp,"%s\r\n", content);
}
fflush(fp);
if ( fpp )
*fpp = fp;
else
fclose(fp);
return bytes;
}

/* ------------------------------------------------------ *
simple functions first:
not_implemented(fd) unimplemented HTTP command
and do_404(item,fd) no such object
------------------------------------------------------ */
not_implemented(int fd)
{
http_reply(fd,NULL,501,"Not Implemented","text/plain",
"That command is not implemented");
}

do_404(char *item, int fd)
{
http_reply(fd,NULL,404,"Not Found","text/plain",
"The item you seek is not here");
}

/* ------------------------------------------------------ *
the directory listing section
isadir() uses stat, not_exist() uses stat
------------------------------------------------------ */
isadir(char *f)
{
struct stat info;
return ( stat(f, &info) != -1 && S_ISDIR(info.st_mode) );
}

not_exist(char *f)
{
struct stat info;
return( stat(f,&info) == -1 );
}

do_ls(char *dir, int fd)
{
DIR *dirptr;
struct dirent *direntp;
FILE *fp;
int bytes = 0;

bytes = http_reply(fd,&fp,200,"OK","text/plain",NULL);
bytes += fprintf(fp,"Listing of Directory %s\n", dir);

if ( (dirptr = opendir(dir)) != NULL ){
while( direntp = readdir(dirptr) ){
bytes += fprintf(fp, "%s\n", direntp->d_name);
}
closedir(dirptr);
}
fclose(fp);
server_bytes_sent += bytes;
}

/* ------------------------------------------------------ *
functions to cat files here.
file_type(filename) returns the 'extension': cat uses it
------------------------------------------------------ */
char * file_type(char *f)
{
char *cp;
if ( (cp = strrchr(f, '.' )) != NULL )
return cp+1;
return "";
}

/* do_cat(filename,fd): sends header then the contents */

do_cat(char *f, int fd)
{
char *extension = file_type(f);
char *type = "text/plain";
FILE *fpsock, *fpfile;
int c;
int bytes = 0;

if ( strcmp(extension,"html") == 0 )
type = "text/html";
else if ( strcmp(extension, "gif") == 0 )
type = "image/gif";
else if ( strcmp(extension, "jpg") == 0 )
type = "image/jpeg";
else if ( strcmp(extension, "jpeg") == 0 )
type = "image/jpeg";

fpsock = fdopen(fd, "w");
fpfile = fopen( f , "r");
if ( fpsock != NULL && fpfile != NULL )
{
bytes = http_reply(fd,&fpsock,200,"OK",type,NULL);
while( (c = getc(fpfile) ) != EOF ){
putc(c, fpsock);
bytes++;
}
fclose(fpfile);
fclose(fpsock);
}
server_bytes_sent += bytes;
}

进程间通信(IPC)

  • 管道
  • select、poll 挂起并等候从多个源端的输入
  • 命名管道
  • 共享内存
  • 文件锁
  • 信号量

select系统调用

系统调用select允许程序挂起,并等待从不止一个文件描述符的输入。

原理:

  • 获得所需要的文件描述符列表
  • 将此列表传给select
  • select挂起直到任何一个文件描述符有数据到达
  • select设置一个变量中的若干位,用来通知你哪一个文件描述符中已经有输入的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//参数说明:
//nfds 需要监听的最大fd值加1
//readfds 等待从此集合包括的文件描述符到来的数据
//writefds 等待向这些文件描述符写数据的许可
//exceptfds 等待这些文件描述符操作的异常
//timeout 超过此时间后函数返回

//返回值
//-1 发生错误
//0 超时
//num 满足需求的文件描述符的数目

void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select同时监视多个文件描述符,在指定情况发生的时候,函数返回。详细一点说,select监听在三组文件描述符上发生的事件:

  • 检查第一组是否可以读取
  • 检查第二组是否可以写入
  • 检查第三组是否有异常发生

每一组的文件描述符被记录到一个二进制位的数组中。这里的numfds恰好等于需要监听的最大的文件描述符加1.

若任一参数为null,select将忽略此参数

通信的选择

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022 ZHU
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信