Linux 编程基础(C 语言)

第一章 Linux 操作系统基础

  1. Linux 的版本:有两种不同的称呼,内核版本、发行版
  2. 需要掌握的内核版编号方式:这种编号方式用于 1.0-2.6,数字由三部分组成:A.B.C
    1. A 代表主版本号,只有内核发生很大变化是,才变化
    2. B 代表次版本号,可以通过其判断该版本是否稳定,偶数 -> 稳定版,奇数 -> 开发版。
    3. C 代表较小的末版本号,表示一些 bug 修复、安全更新、新特性和驱动的次数。

第二章 Linux 平台项目开发环境

  1. vi 的三种工作模式,插入模式、命令模式、末行模式
    1. 插入模式:也称为编辑模式,进入插入模式后,屏幕的最后一行会出现 "编辑" 或 "insert" 字样。要注意的是,刚启动 vi 时是处于命令模式下。笔者一般是按 "i" 键进入插入模式。
    2. 命令模式:等待用户输入命令的模式,类似于图像界面中用鼠标点击某些菜单选项的感觉。
    3. 末行模式:在命令模式下键入 ":",为了完成文档的一些辅助功能,q:若文档没有修改过直接退出 vi,否则会有提示信息并且不会退出 vi;wq:保存并退出;q!:直接退出,无论内容有没有修改,都不会保存。
  2. GCC 编译器的基本用法
    1. 源文件生成可执行文件

      gcc -o main main.c
      或
      gcc main.c -o mian
      
2. 源文件生成目标文件
    ```
    gcc -c main.c
    ```
3. 源文件生成汇编文件
    ```
    gcc -s main.c
    ```
  1. Makefile 文件的内容
    1. 语法格式

      目标文件:依赖文件列表
      <tab>更新目标文件使用的命令
      
    2. 举例说明:编译 main.c

      main:main.o
          gcc -o main main.o
      main.o:main.c
          gcc -c main.c
      clean:
          rm main main.o
      
3. 这个没什么好说的,就是个变量的替换(实在不能理解就背下来)
    ```
    OBJ=stuscore
    SRC=stuscore.c in.c out.c cal.c
    $(OBJ):$(SRC)
        gcc -o $@ $^
    clean:
        rm $(OBJ)
    ```

第三章 Linux 程序设计初步

  1. C 进程内存布局,要注意的是:堆是动态分配的内存区域,大小不固定,可动态的增加(malloc 函数)和释放(free 函数);栈用来保存函数临时创建的局部变量(不包括函数内部的 static 变量),另外在函数被调用时,函数参数也会被压如发起调用函数的进程栈中,等待调用结束后,函数的返回值会被放到栈中。

  2. 特殊修饰变量的存储位置

    变量形式存储位置
    auto 变量
    stati 全局变量已初始化在数据段,未初始化在 BSS
    extern 变量extern 变量用于声明引用定义在别处的全局变量,该变量已经初始化在数据段,未初始化在 BSS
    register 变量CPU 寄存器
  3. 命令行参数

    • 用户在执行程序时输入的 包含程序名称在内直到回车之前 的所有数据成为命令行参数
  4. 环境变量修改,修改环境变量的格式与定义环境变量的格式相同,对于 export 命令的使用也相同。如果修改变量的同时还需要包含原来的值,则需要事先引用原来的值,如 PATH 变量中保存了可执行文件的搜索路径,使用冒号分隔,原来保存了一些路径值,如果需要增加一个当前目录,则要进行如下修改:

    export PATH=$PATH:.
    
  5. 函数 unsetenv

    项目描述
    头文件#include <stdlib.h>
    原型void unsetenv(const char *name);
    功能删除一个环境变量
    参数name:要删除的环境变量名
    返回值执行成功时,返回 0;失败时,返回 -1,并设置 errno
  6. 时间管理

    1. Linux 内核提供的时间是从国际标准时间 (UTC) 公元 1970 年 1 月 1 日 0 时 0 分 0 秒开始以来经过的秒数,被称为日历时间,数据类型为 time_t

    2. 系统调用 time

      项目描述
      头文件#include <time.h>
      原型time_t time(time_t *t);
      功能得到当前的日期和时间
      参数t:time_t 类型指针,指向获得的当前日期和时间
      返回值time_t 类型,代表获得的当前时间
      备注调用形式可以有两种,一种是通过参数获得,一种是通过返回值获得(如下方代码)
      time_t time;
      time = time(NULL); 或 time(&time);
      
    3. 函数 ctime

      项目描述
      头文件#include <time.h>
      原型char *ctime(const time_t *t);
      功能把 time_t 转换为本地时间字符串
      参数t:time_t 类型指针,代表要转换的 time_t 类型时间
      返回值time_t 类型,代表获得的当前时间
    4. 知识验证

      • 获取当前时间,按要求输出
      1. 通过结构体方式输出 "年 - 月 - 日: 时: 分: 秒: 星期"
      2. 通过字符串输出
  7. 项目:设置环境变量

第四章 文件 I/O

  1. UNIX 文件系统结

    引导区(Boot Block)超级块(Super Block)索引节点表(Inode List)数据区(Data Blocks)
  2. 文件访问权限中的符号常量,Linux 系统为方便识别各用户的权限,使用 S_IPwww 格式的常量代表哥哥用户的各个权限,每个常量都是一个数字,多个权限使用位或运算符 "|" 连接。其中 P 可以替换为读(R)、写(W)、执行(X),www 可以替换为文件主人(USR)、组成员(GRP)、其他用户(OTH)

    八进制描述
    S_ISUID0004000SUID 权限
    S_ISGID0002000SUID 权限
    S_ISVTX0001000sticky 权限
    S_IRWXU00700文件主人权限掩码
    S_IRUSR00400主人有读权限
    S_IWUSR00200主人有写权限
    S_IXUSR00100主人有执行权限
    S_IRWXG00070同组用户权限掩码
    S_IRGRP00040同组用户有读权限
    S_IWGRP00020同组用户有写权限
    S_IXGRP00010同组用户有执行权限
    S_IRWXO00007其他用户权限掩码
    S_IROTH00004其他用户有读权限
    S_IWOTH00002其他用户有写权限
    S_IXOTH00001其他用户有执行权限
  3. 访问文件的内核数据结构(教材 P82)

    1. 每个进程在简历好时,都默认打开三个文件:标准输入设备文件、标准输出设备文件、标准错误输出设备文件。这三个文件登记在用户打开文件描述服务表中的前三项。因此这三个文件的文件描述服务分别是 0、1、2.Linux 系统中定义了这三个文件描述符

      # define STDIN_FILENO 0 // 标准输入设备文件
      # define STDOUT_FILENO 1 // 标准输出设备文件
      # define STDERR_FILENO 2 // 标准错误输出文件
      
  4. 文件基本 I/O 操作

    1. 系统调 open/create

      项目描述
      头文件#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>
      原型int open(const char *path,int flags); int open(const char *path,int flags,mode_t mode); int open(const char *path,mode_t mode);
      功能按照 flags 打开 path 指定的文件,或者创建权限为 mode 的新文件 path
      参数path:要打开或创建的文件名(包名);flags:打开方式;mode 当要创建一个新文件时 mode 指定权限
      返回值成功时,返回一个问价描述符;失败时返回 -1,并且 errno 为错误码
    2. 文件描述符。每个进程都有一个用户文件描述符表,用来记录该进程已经打开的文件。当一个进程通过 open() 打开一个文件时,系统根据 path 找到该文件的 inode,校验该文件的属性信息是否可以打开。如果可以打开,则该进程的用户文件描述符表中查找最小可用表项的索引作为文件描述符返回。所以 open 函数总是得到一个最小可用的文件描述符。

    3. 打开文件方式 flags

      flags含义
      O_RDWR读写方式
      O_CREAT如果 path 指定的文件不存在,就创建该文件
      O_TRUNC如果 path 指定的文件是已经存在的普通文件,并且打开方式是可写的,就将文件长度截为 0. 对于 FIFO 文件或终端设备文件,该方式被忽略
  5. whence 的取值

    | --- | --- | --- | | whence 参数 | 读写指针移动方式 | 读写指针移动结果 | | SEEK_SET | 从文件开始位置移动 | 即为 offset 的值 | | SEEK_CUR | 从文件当前位置开始移动 | 当前读写指针的值 +offset | | SEEK_END | 从文件末尾位置移动 | 文件长度 +offset |
    6. 知识验证,同一个程序,两次打开同一个文件进行读写操作。

    • 为了证明:两次调用 open 函数得到的文件描述符不同

第五章(文件属性管理)

  1. 系统调用 stat/fstat/lstat

    1. int stat(const char * file_name, stuct stat * buf);
    2. int fstat(int filedes, struct stat * buff);
    3. int lstat(const char * file_name, struct stat * buff);
    4. 功能描述:获取指定文件的文件属性,并将属性存入 stat 的结构体 buff 中
  2. 硬链接与符号链接

    • 硬链接和符号链接都是指向另一个已存在文件的链接,符号链接文件相当于 Windows 中的快捷方式
    1. 硬链接命令:ln 原文件 硬链接文件(不能对目录创建硬链接!!!)
    2. 符号链接命令:ln -s 原文件 / 目录 符号链接文件

第六章(目录文件管理)

  1. 函数 readdir
    1. 原型:struct dirent * readdir(DIR * dir)
    2. 功能:该函数从 dir 指向的目录文件中读取一个目录项,并返回一个指向该目录项的指针
    3. 参数:dir:指向要读取的目文件的指针,dir 可以使用函数 opendir() 打开目录文件时得到
    4. 返回值,成功返回 dirent 结构体指针;失败或读到目录文件末尾,返回 NULL

第七章(进程控制)

  1. 用户标识
  2. 系统调用 fork
    1. 原型:pid_t fork(void)
    2. 功能:创建一个子进程
    3. 参数:无
    4. 返回值:成功时,在父进程返回 PID,在子进程返回 0;失败时,返回 -1 并置错误代码 errno
    5. 由父进程调用一次,但是返回值有两次。
    6. 用 fork 创建的 子进程是父进程的复制品 ,子进程拥有父进程的数据空间、堆、栈的一份拷贝,父子进程 不共享 这部分存储空间
  3. 系统调用 vfork()
    1. 原型:pid_t vfork(void)
    2. 功能:创建一个子进程, 并阻塞父进程
    3. 参数:无
    4. 返回值:成功时,在父进程返回 PID,在子进程返回 0;失败时,返回 -1 并置错误代码 errno
    5. 由父进程调用一次,但是返回值有两次。
    6. 用 vfork 创建子进程,不会将父进程的数据空间复制到子进程中, 子进程与父进程共享数据空间 ,父进程会等待子进程先执行。子进程中如果不调用 exec 函数,则需要调用 _exit 系统调用。
  4. 知识验证,通过 waitpid 等待指定的子进程结束

第八章(线程)

  1. 线程对比进程

    1. 创建新线程系统开销小

    2. 多线程间共享地址空间,线程间切换效率高

    3. 通信方面,线程间通信更方便和省时

    4. 可以提高程序响应速度

    5. 多线程可以提高多核 cpu 执行效率

    6. 在大型程序中,采用多线程设计可以改善程序结构

    7. 需要频繁创建和销毁就选择多线程

    8. 需要大量计算,优先考虑多线程

    9. 并发处理间的相关性强的,优先考虑多线程,反之,优先考虑多进程


    10. 若编程和调试都相对简单地程序,可以优先开了多进程

    11. 若对可靠性有一定要求,多进程会更加安全可靠,因为多线程共享进程的地址空间,对资源的同步、互斥的访问时易产生错误

  2. 知识验证,利用互斥锁,实现多线程对临界资源的同步互斥问题

第九章(信号与管道)

  1. 什么是信号
    • 在 Linux 系统中,信号是一个 32 位整型值,代表一个简单信息。每个信号都是以一个 SIG 开头的名字,实际上是系统定义的宏,如 SIGINT 代表中断信号,其值为 2
  2. 系统调用 alarm
    1. 原型:unsigned int alarm(unsigned int seconds);
    2. 功能:设置一个定时器
    3. 参数(重点):seconds 为定时器的时间
    4. 返回值(重点):返回上一个定时器剩余时间,如果没有其他定时器则返回 0
    5. 解释返回值;每个进程只拥有一个定时器时间,如果在调用 alarm 之前已经设置了定时器且还没有超时,则此次 alarm 调用返回上一次所设时间的剩余值,并重新设置定时器时间。
  3. 进程如何响应信号
    1. 忽略信号
    2. 执行系统默认动作
    3. 捕捉信号
  4. 管道
    • 我们这里讨论的管道,也称为无名管道,也就是说通信的进程不需要为管道命名就可以访问,通过管道传数据。使用管道通信的两个进程一定要有共同的祖先。
  5. FIFO 也称为命名管道。允许不想管进程通信。

第十章(略)

第十一章(网络编程)

  1. 字节序

  2. 套接字地址结构:ip+port

  3. 面向连接(tcp)套接字通信过程

  4. 面向无连接(udp)套接字通信过程 尴尬!这个拍的图片不见了,就参考参考教程上的吧!

  5. TCP 和 UDP 的 服务器端 代码实现(重点)(教材 P269-P273)

// TCP server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

# define SERVERPORT 3333 // 服务器端口号
# define BACKLOG 10 // 最大同时连接请求数


int main(int argc, char const *argv[])
{
    int sockfd, connfd; // sockfd:监听套接字;connfd:链接套接字
    struct sockaddr_in my_addr; // 本机地址
    struct sockaddr_in remote_addr; // 客户端地址
    int sin_size, val = 1, status;

    //创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);  //SOCK_STREAM表示使用TCP
    if (sockfd == -1)
    {
        perror("socket");
        exit(1);
    }
    // 设置地址端口可以重用
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val));
    // 设置本地地址信息
    my_addr.sin_family = AF_INET; // 协议簇使用ipv4
    my_addr.sin_port = htons(SERVERPORT); // 端口号
    my_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ip地址
    bzero(&(my_addr.sin_zero),8); // 填充0 
    // 绑定地址到套接字描述符上
    status = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
    if (status == -1)
    {
        perror("bind");
        exit(1);
    }
    // 在地址端口监听
    status = listen(sockfd,BACKLOG);
    if (status == -1)
    {
        perror("listen");
        exit(1);
    }
    while (1)
    {
        sin_size = sizeof(struct sockaddr_in);
        // 等待客户端的接入
        connfd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size);
        if (connfd == -1)
        {
            perror("accept");
            exit(1);
        }
        // 当客户端接入了,就可以做收发操作了...
        printf("有客户端接入");
    }
    return 0;
}

// DUP server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

# define SERVERPORT 3333 // 服务器端口号
# define MAXDATASIZE 100

int main(int argc, char const *argv[]) {
    int sockfd; // sockfd: 监听套接字
    struct sockaddr_in server;
    struct sockaddr_in client;
    int sin_size;
    int num;
    char msg[MAXDATASIZE];
    char sbuf[100] = " ";
    return 0;
    // 创建 UDP 套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)  //SOCK_DGRAM 表示使用 UDP
    {
        perror("socket");
        exit(1);
    }
    // 设置本地地址信息
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET; // 协议簇使用 ipv4
    server.sin_port = htons(SERVERPORT); //
    server.sin_addr.s_addr = htonl(INADDR_ANY); //
    // 绑定地址到套接字描述符上
    if (bind(sockfd,(struct sockaddr *)&server, sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    sin_size = sizeof(struct sockaddr_in);
    while (1)
    {
        // 从监听端口读出数据
        num = recvfrom(sockfd, msg, MAXDATASIZE,0,(struct sockaddr*)&client, &sin_size);
        if (num < 0)
        {
            perror("recvfrom");
            exit(1);
        } 
        msg[num] = '\0'; // 添加消息结束符
        // 后面就可以根据 client 得到客户端的信息,即可实现通信。
    }
}