4.1 UNIX_C

UNIX C编程

一、开发环境

  • C/C++/数据结构/算法:平台无关、算法逻辑
  • UC/Win32/Android/IOS:平台相关、系统调用
  • 嵌入式/驱动/移植:硬件相关、硬件接口

1.Unix操作系统

1.简介

  • 美国AT&T公司,贝尔实验室,
  • 1971年,
  • 肯.汤普逊、[丹尼斯.里齐]。
  • PDP-11,多用户,多任务,多处理器架构。
  • 高安全性、高可靠性、高稳定性。
  • 关键业务系统,商业应用。
  • 移动终端、嵌入式设备。

2.三大版本派系

(1).System V

  • AIX,IBM,银行
  • Solaris,SUN->Oracle,电信
  • HP-UX,惠普,教育,科研
  • IRIX,图形工作站

(2).Berkley

  • FreeBSD
  • NetBSD
  • OpenBSD
  • Max OS X

(3).Linux

  • Minix:迷你版UNIX系统
  • Linux:GPL,免费开源,商用服务器(RedHat),桌面(Ubuntu),嵌入式(Android)

2.Linux操作系统

1.简介

  • 类Unix操作系统,免费开源。
  • 不同发行版本使用相同内核。
  • 严格意义上讲,Linux仅指操作系统内核。
  • 隶属于GNU(GNU Not Unix)工程。
  • 发明人:Linus Torvalds。
  • 标志:名为Tuxedo的小企鹅。

2.POSIX标准

  • IEEE和ISO合作指定。
  • Portable Operating System Interface for
  • Computing Systems
  • 统一系统编程接口规范
  • 基于POSIX接口的开发是可移植的。

3.GPL

  • 通用公共许可证
  • 允许对某成果及其派生成果的重用、修改和复制,对所有人都是自由的,但不能声明做了原始工作。

4.版本

(1).早期版本:0.01,0.02,...,1.0

(2).旧计划:介于1.0和2.6之间,A.B.C

​ A:主版本号,内核大幅更新

​ B:次版本号,内核重大修改,奇数测试版,偶数稳定版

​ C:补丁序号,内核轻微修订

(3).2003年12月发布2.6.0以后:发布周期缩短,A.B.C-D.E

​ D:构建次数,反映极微小的更新

​ E:描述信息

​ rc/r - 候选版本,后面跟的数字表示第几个候选版本,数字越大越接近正式版

​ smp - 对称多处理器

​ pp - Red Hat测试版

​ EL - Red Hat正式企业版

​ mm - 针对最新技术的体验版本

​ fc - Fedora Core版本

​ $ cat /proc/version

5.主流Linux发行版本

  • Ubuntu - 大众化
  • Linux Mint - 时尚
  • Fedora - RedHat的桌面版,应用丰富
  • openSUSE - 外观华丽,欧洲国家受欢迎
  • Debian - 开放,自由,免费
  • Slackware - 简洁,朴素,专业化
  • RedHat - 经典,支持丰富

3.GNU编译工具-GCC

  1. 支持多种编程语言

  2. C、C++、Objective-C、Java、Fortran、Pascal、ADA

  3. 支持多种平台

  4. Unix、Linux、Windows

  5. 构建(Build)过程

  6. 编辑->预编译->编译->汇编->链接

    1)编辑:vi hello.c

    2)预编译:gcc -E hello.c -o hello.i

    3)编译:gcc -S hello.i

    4)汇编:gcc -c hello.s

    5)链接:gcc hello.o -lc -o hello

  7. GCC工具链。

  8. gcc版本

  9. gcc -v

  10. 文件后缀

  11. .h - C语言源代码头文件

  12. .c - 预处理前的C语言源代码文件
  13. .i - 预处理后的C语言源代码文件
  14. .s - 汇编语言文件
  15. .o - 目标文件
  16. .a - 静态库文件
  17. .so - 共享库(动态库)文件

  18. 编译单个源程序

  19. gcc [选项参数] 文件

  20. -c - 编译+汇编
  21. -o - 指定输出文件路径
  22. -E - 预编译
  23. -S - 编译,产生汇编文件
  24. -pedantic - 对程序中不符合标准C的代码给出 警告
  25. -Wall - 产生尽可能多的警告
  26. -Werror - 将警告作为错误
  27. -x - 指定源代码的语言
  28. -g - 生成调试信息
  29. -O0/O1/O2/O3 - 优化等级,缺省O1,O0表示不做优化,某些系统上的gcc支持Os,相当于O2.5

  30. 编译多个源程序

  31. gcc [选项参数] a.c b.c c.c d.c

  32. makefile/make
  33. 头文件的作用

​ a)声明外部变量和函数。

​ extern int g_x;

​ int add (int, int);

​ b)定义宏、类型别名、自定义类型。

#define PAI 3.14159
typedef unsigned int UINT;
struct Student {
    char name[128];
    int age;
};

​ c)公共头文件

​ #include

​ #include

​ #include

​ d)头文件卫士,防止因钻石包含导致重定义

    a.h
   /    \
b.h     c.h
   \    /
     d.c
  • gcc的-I选项:指定头文件的附加搜索路径。

    #include <...>

    系统目录->-I指定的目录

    通常的系统目录:

    /usr/include

    /usr/local/include

    /usr/lib/gcc/i686-linux-gun/4.6/include

    /usr/include/c++/4.6, C++编译器使用

  • 预处理指令

#include
#define PAI 3.14
#undef PAI
#if
#ifdef PAI
#ifndef
#else
#elif
#endif
##
#
#error // 产生错误,结束并终止预编译过程
#warning // 产生警告,并不终止预编译过程
#line // 指定行号
#pragma // 为处理器提供额外信息
#pragma GCC dependency <文件> // 如果<文件>比当前文件新,产生警告
#pragma GCC poison <标识> // 如果程序中出现<标识>,直接报错
#pragma pack(1/2/4/8) // 对结构体型变量,按1/2/4/8字节做对齐和补齐
  1. 预定义宏
__BASE_FILE__ // 正在被编译的源文件名
__FILE__ // 所在的文件名
__LINE__ // 所在行号
__FUNCTION__ // 所在函数名
__func__ // 同__FUNCTION__
__DATE__ // 编译日期
__TIME__ // 编译时间
__INCLUDE_LEVEL__ // 包含层数,基0
__cplusplus // 是否C++编译器,是C++编译器此宏为1,不是C++编译器此宏无定义
  1. 环境变量
C_INCLUDE_PATH
CPATH //C头文件的附加搜索路径
CPLUS_INCLUDE_PATH //C++头文件的附加搜索路径
LIBRARY_PATH //链接时库文件搜索路径
LD_LIBRARY_PATH //运行时库文件搜索路径

4.库

  1. 为什么需要库?

    二进制形式目标模块的包。

    a.o \

    b.o > abc.a ——库

    c.o /

  2. 库的类型

    (1).静态库:扩展名.a。库中所封装的二进制代码,在链接阶段被复制到调用模块中。

    (2).共享库:扩展名.so。库中所封装的二进制代码,在链接阶段并不被复制到调用模块中,被嵌入到调用模块中的仅仅是被调用函数在共享库中的相对地址。在运行时,根据这个地址动态地执行共享库中的代码。

  3. 静态库

    (1).编辑源程序:.c/.h

    (2).编译成目标文件:gcc -c xxx.c -> xxx.o

    (3).打包成静态库文件:ar -r libxxx.a xxx.o ...

    $ gcc -c calc.c
    $ gcc -c show.c
    $ ar -r libmath.a calc.o show.o
    

    ar [选项] 静态库名 目标文件列表

    -r:将目标文件插入到静态库中,若已存在

    ​ 则更新

    -q:将目标文件追加到静态库尾部

    -d:从静态库中删除指定的目标文件

    -t:列表显示静态库中的目标文件

    -x:将静态库展开为目标文件

    (4).调用静态库

    $ gcc main.c libmath.a (直接法)
    $ gcc main.c -lmath -L. (参数法)
    $ export LIBRARY_PATH=$LIBRARY_PATH:.
    $ gcc main.c -lmath (环境法)
    

    为了使环境的设置持久化,把设置环境变量

    的命令发到~/.bash_profile中。

  4. 共享库

    (1).创建共享库

    A. 编辑源程序:.c/.h

    B. 编译成目标模块:

    gcc -c -fpic xxx.c -> xxx.o
    

    C. 链接成共享库:

    ​ gcc -shared xxx.o ... -o libxxx.so

    ​ PIC,Position Independent Code,位置无关码。可执行程序加载共享库时,可将其映射到其地址空间的任何位置。

    ​ -fPIC - 大模式,代码量大,速度慢,所有平台都支持。

    ​ -fpic - 小模式,代码量小,速度快,仅一部分平台支持,如Linux。

    (2).使用共享库

    A. 静态加载

$ gcc main.c libmath.so
$ gcc main.c -lmath -L.
$ export LIBRARY_PATH=$LIBRARY_PATH:.
$ gcc main.c -lmath

​ 运行时需要保证LD_LIBRARY_PATH环境变量中包含共享库所在的路径。

​ B. 动态加载

#include <dlfcn.h>

​ a.加载共享库

void* dlopen (
  const char* filename,
  int flag
);
filename:若只给共享库文件名,则通过
LD_LIBRARY_PATH环境变量搜索共享库。若给共享库路径,则按照路径加载,不使用环境变量。
flag:
RTLD_LAZY - 延迟加载,什么时候使用共享库再实际载入之。
RTLD_NOW - 立即加载。
成功返回共享库句柄,失败返回NULL

​ b.获取函数地址

void* dlsym (
  void* handle, // 共享库句柄
  const char* symbol // 函数名
);
成功返回函数地址,失败返回NULL

​ c.卸载共享库

int dlclose (
  void* handle // 共享库句柄
);
成功返回0,失败返回非零。

​ d.获取错误信息

char* dlerror (void);
返回错误信息字符串,没有错误信息返回NULL。
gcc main.c -ldl

5.辅助工具

  • nm:查看目标文件、可执行文件、静态库、共享库中符号列表。
  • ldd:查看可执行程序或共享库的动态依赖。
  • ldconfig:事先把共享库的路径信息写到
  • /etc/ld.so.conf配置文件中,ldconfig根据该配置文件生成/etc/ld.so.cache缓冲文件,并将该缓冲文件读入内存,提高动态库的加载效率。系统启动时自动执行ldconfig,若修改了共享库配置,则需要手动执行该程序,更新缓冲。
  • strip:通过删除符号表和调试信息,给目标文件、可执行文件、库文件减肥。
  • objdump:对机器指令做反汇编。

6.错误处理

1.通过函数的返回值表示错误

(1). 返回合法值表示成功,返回非法值表示失败。

(2). 返回有效指针表示成功,返回空指针(NULL/0xFFFFFFFF)表示失败。

(3). 返回0表示成功,返回-1表示失败,如果有需要返回给调用者的数据,可以通过指针型参数向其输出。

(4). 如果一个函数永远不会失败,也没数据需要提供给调用者,可以没有返回值。

2.通过错误码获得函数失败的原因

#include <errno.h> // extern int errno;

(1). 通过errno全局变量获取出错原因。

(2). 将errno转换为一个字符串:

#include <string.h>
char* strerror (int errnum);
#include <stdio.h>
void perror (const char* s);
printf ("%m");
所有的错误码都非零,errno == 0表示无错误。

(3). errno在函数执行成功的情况下不会被修改,因此不能以errno非零作为发生错误的判断依据,除非在调用函数前人为将其复位为0。

(4). errno是一个全局变量,其值随时有可能发生变化,线程不安全。

7.环境变量

1.环境表

(1). 每个进程都会接收到一张环境表,是一个以NULL指针结尾的字符指针数组。

(2). 全局变量environ保存了环境表的首地址。

(3). main函数的第三个参数就是环境表的首地址。

2.环境变量函数

  • #include
  • 环境变量:\=\
  • getenv - 根据name获得value
  • putenv - 以\=\形式设置环境变量。如果name不存在,就添加,存在修改原来的value
  • setenv - 根据name设置value,若name以存在,根据参数决定是否覆盖原value
  • unsetenv - 删除环境变量
  • clearen - 清空环境变量,environ == NULL

二、内存管理

1.进程映像

1.程序是保存在磁盘上的可执行文件,如:a.out、ls、gcc、qq.exe。

2.运行程序时,需要把磁盘上的可执行文件,加载到内存中,形成进程。

3.一个程序(文件)可以同时存在对个进程(内存)。

4.进程在内存空间中的布局就是进程映像。从低地址到高地址依次是:

  • 代码区(text):可执行指令、字面值常量、具有常属性且初始化的全局和静态变量。只读。

  • 数据区(data):不具常属性且初始化的全局和静态变量。

  • BSS区(bss):未初始化的全局和静态变量。

  • 进程一加载此区即被清0。

  • 堆区(heap):动态内存分配。

  • 栈区(stack):非静态局部变量。

  • 命令行参数和环境变量区

在堆区和栈区之间会留有一段空隙,一方面为堆和栈的增长预留空间,同时共享库、共享内存也会占用这个区域。

---- 命令行参数与环境变量 ----
         环境变量:0xbfb1aebc
       命令行参数:0xbfb1aeb4
-------------- 桟  -----------
       常局部变量:0xbfb1adf8
       前局部变量:0xbfb1adfc
       后局部变量:0xbfb1ae00
-------------- 堆 ------------
         后堆变量:0x97d2018
         前堆变量:0x97d2008
------------- BSS ------------
未初始化全局变量:0x804a03c
未初始化静态变量:0x804a038
------------ 数据 ------------
   初始化静态变量:0x804a028
   初始化全局变量:0x804a024
------------ 代码 ------------
       常静态变量:0x8048a50
       字面值常量:0x80487b4
       常全局变量:0x80487b0
             函数:0x80484f4
PID = 3163

2.虚拟内存

  1. 每个进程都有各自独立的4G字节的虚拟地址空间。

  2. 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。

  3. 虚拟内存到物理内存的映射有操作系统动态维护。

  4. 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序使用比实际物理内存更大的地址空间。

  5. 4G的进程空间分为两部分,0~3G-1为用户空间,3G~4G-1为内核空间。

  6. 用户空间中代码不能直接访问内核空间中的代码和数据,但是可以通过系统调用进入内核态,间接地与内核交互。

  7. 对内存的越权访问,或访问未建立映射的虚拟内存,将会导致段错误。

int* p;
*p = 100;
--------------
int a;
int* p = &a;
*p = 100;
--------------
int* p = malloc (sizeof (int));
*p = 100;
  1. 用户空间对应进程,进程一切换,用户空间随即变化。内核空间由操作系统内核使用,不会随进程切换而变化。内核空间由内核根据独立且唯一的页表init_mm.pgd进行映射,而用户空间的页表则每个进程一份。

  2. 每个进程的内存空间完全独立,因此在不同进程之间交换虚拟地址毫无意义。

  3. 标准库的内存分配函数(malloc/calloc/realloc)需要用一套数据结构维护动态分配的内存,因此会分配比实际要求的内存多12个字节的内存,用户存储某些控制信息。该信息一旦被破坏,将导致后续操作,如free等,出现异常。

  4. 虚拟内存到物理内存的映射以页(4096字节)为单位。通过malloc函数首次分配内存,至少映射33页。即使通过free函数,释放掉全部动态分配的内存,最初的33页仍然保留,直到进程退出这33页才会真正被解除映射。

#include <unistd.h>
int getpagesize (void);
返回内存页的字节数。
char* pc = malloc (sizeof (char));
      |
      v<------------ 33页 ----------->|
------+-----+----------+--------------+----
      |1字节| 控制信息 |                       |
------+-----+----------+--------------+----
  ^      ^         ^           ^          ^
段错误    OK    后续错误          不稳定   段错误

3.内存管理APIs

#include <unistd.h>
void* sbrk (
    intptr_t increment // 内存增量(字节)
);
返回上次调用sbrk/brk函数后的末尾指针,失败返回-1

increment取值:

0 - 获取末尾指针

>0 - 增加内存空间

<0 - 释放内存空间

内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。

sbrk函数根据增量参数调整该指针的位置,同时返回该指针被调整之前的位置。

若发现页耗尽或空闲,则自动追加或解除页映射。

#include <unistd.h>
int brk (
    void* end_data_segment // 内存块尾地址
);

成功返回0,失败返回-1。内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。brk函数根据指针参数设置该指针的位置。若发现页耗尽或空闲,则自动追加或解除页映射。sbrk/brk底层维护一个指针位置,以页(4K)为单位映射和解映射内存,并根据参数调整所维护的指针位置。简便起见,用sbrk分配内存,用brk释放内存。

简化版malloc/free的实现。

#include <sys/mman.h>
void* mmap (
    void* start, // 映射区首地址,
                 // NULL系统自动选择
    size_t length, // 映射区长度(字节)
                 // 按页取整
    int prot, // 映射权限
    int flags, // 映射标志
    int fd, // 文件描述符
    off_t offset // 文件偏移量
);

成功返回映射区首地址,失败返回MAP_FAILED(-1)。

prot取值:

  • PROT_EXEC - 映射区可执行

  • PROT_READ - 映射区可读

  • PROT_WRITE - 映射区可写

  • PROT_NONE - 映射区不可访问

flags取值:

  • MAP_FIXED - 若在start上无法创建映射, 返回失败,无此标志则自动调整

  • MAP_SHARED - 对映射区的写操作直接反映到文件中

  • MAP_PRIVATE - 对映射区的写操作只反映到内存中,不进文件

  • MAP_ANONYMOUS - 内存映射,将虚拟地 址映射到物理内存中

  • MAP_DENYWRITE - 拒绝其它对文件的写入

  • MAP_LOCKED - 锁定映射区域

int munmap (
    void* start, // 映射区首地址
    size_t length // 映射区长度(字节)
    // 按页取整
);

成功返回0,失败返回-1。

  • mmap/munmap:底层不维护任何东西,只是返回一个建立映射的虚拟地址。

  • brk/sbrk:底层维护一个指针,记录堆尾。

  • malloc/free:底层维护一个双向链表,存储内存块的控制信息。


总结:

Unix内存管理的函数:

  • malloc() free() - 标C函数

  • sbrk() brk() - Unix的系统函数

  • mmap() munmap() - Unix的系统函数

malloc申请内存时,如果是小块内存,一次映射33个内存页(物理内存),分配申请的数量(虚拟内存)。malloc()除了分配申请的内存之外,还需要额外的空间存储附加数据。 如果申请的是大块内存,一次映射比申请的稍多的内存页,分配申请的数量。

sbrk()和brk()是Linux的系统函数,本身具备分配和回收内存的能力。sbrk()分配内存简便,brk()回收内存方便。sbrk()、brk()没有额外的附加数据,也没有33页的映射。一次就是1页,底层维护一个位置,用位置的变化分配和回收内存。

mmap()和munmap()在内存分配/回收时提供了更多的选择,是一个可管理的内存分配方式。可以用第一个参数设定分配的首地址,也可以用第三个参数设定权限,第四个参数包括:

  • MAP_SHARED/MAP_PRIVATE 设定是否共享(只对映射文件有效)。

  • MAP_ANONYMOUS 设定映射物理内存

默认情况下,mmap()映射文件。

三、文件系统

  1. 系统调用:因为用户空间不能直接访问内核空间,想完成功能又必须得到内核的支持。因此,内核层提供了系统调用,做用户空间进入内核空间的桥梁。系统调用是 一系列的函数,包括各种系统的功能。以后我们接触的大多数都是系统调用。

  2. 文件操作:非常常用的函数,包括读写函数和 非读写函数。

  3. 在Linux系统中,几乎一切都是文件。目录、内存、各种硬件设备都可以看成文件。比如:/dev/tty 代表键盘和显示器。

echo hello 默认输出到显示器上
echo hello > a.txt 把输出改到a.txt中
echo hello > /dev/tty 把输出改到显示器中
cat /dev/tty 直接从键盘读数据
ctrl+C 退出
vi ../ 查看上层目录的内容
  1. 标C用FILE*(文件指针)代表一个打开的文件,UC用文件描述符代表一个打开的文件。文件描述符其实就是一个非负整数,文件描述符自身不存储任何文件信息,信息都存在 文件表中,文件描述符对应文件表。对应Linux来说,一个进程最多同时打开256个文件,描述符从0开始计算。0、1、2系统已经占用,程序员不能使用,代表标准输入、标准输出和标准错误。程序员的文件描述符从3开始。

  2. 文件读写函数:

  3. open() - 打开一个文件,返回文件描述符

int open(char* filename,int flags,...)
参数:filename 是打开文件的路径(包括文件名)
     flags 标识,主要有以下宏定义:
权限标识:O_RDONLY  
        O_WRONLY O_RDWR 
        权限标识必选其一
附加标识:O_APPEND 用追加的方式打开(从文件尾开始写,读文件一般不用)
创建标识:O_CREAT 存在就打开,不存在就新建       
        O_TRUNC 文件存在时清空所有数据(谨慎)
        O_EXCL 不存在就新建,存在不打开,而是返回-1,代表出错。
第三个参数 ... 叫可变长参数,代表0-n个任意的参数,只有在新建文件时,才使用。传入新文件的权限。
注: 第三个参数是文件在硬盘上的权限(某些权限可能被系统屏蔽)。O_RDONLY等是文件描述符的权限。返回文件描述符,失败返回-1。多个选项 用 位或 | 连接。
  • read()/write() - 读/写一个文件
int read(int fd,void* buf,size_t size)
int write(int fd,void* buf,size_t length)
参数:fd文件描述符,就是open()的返回值
    buf是读/写的首地址,任意类型都可以
    size是buf的大小(有可能读不满)
    length是真实想要写入的字节数(满)
返回有三种:
    正数 - 真实读到/写入的字节数
    0  - 读到文件尾/什么都没写
    -1 - 出现错误
注: read()返回0 通常用于循环读文件的退出。
vi编辑器用wq保存退出时,自动加一个结束符,可以被cat换行,但是用write()没有加。
  • close() - 关闭文件
  • ioctl() - 是设备驱动程序中对设备的I/O通道进行管理的函数。

  • 关于字符串的处理

    C程序员定义字符串有三种:

"abc" 字面值,本身不是变量。

char buf[length]; 字符数组

char* st ; 字符指针

字符串以'\0'做结尾。数组可以看成常指针(不能改地址,只能初始化),某些时候和指针有区别(比如sizeof)。

具体操作见 string.c

  1. time a.out可以查看a.out的运行时间

  2. 所有的标C函数都在用户层定义了输入/输出缓冲区,作用就是累计到一定量以后再进入内核读/写一次。UC函数都没有定义缓冲区,但可以由程序员自定义缓冲区提升效率。

  3. 文件读写的位置用偏移量记录,在文件表中存储了偏移量。函数lseek()可以随意移动偏移量。

int lseek(int fd,int offset,int whence)
参数:fd 就是文件描述符
offset 是偏移量
whence是偏移的开始位置
返回当前位置到文件头的偏移量,失败返回-1.
注: 
whence + offset可以确定位置,whence包括:
SEEK_SET - 从头开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从结尾开始
lseek()的返回值可以计算文件的大小。
  1. 文件操作的非读写函数

    (1). dup() dup2() - 复制文件描述符,但不复制文件表

    dup()由系统选定新描述符的值,dup2()由程序员指定新描述符的值,如果指定值已经被使用,先关闭再使用(不安全的隐患)。

    内存用虚拟内存地址管理,硬盘上的文件/目录如何管理,用inode管理。i节点可以认为是文件在硬盘上的地址。ls -i可以查看文件/目录的i节点。

    (2). 函数fcntl()实现很多的功能,由参数cmd决定,常见的应用:

    可以复制文件描述符

    可以设置/获取描述符的状态

    可以设置文件锁

    int fcntl(int fd,int cmd,...)
    参数: fd 文件描述符
        cmd 命令,可以设置fcntl()完成什么功能,常用功能:
    F_DUPFD(long) - 复制文件描述符,传入第三个参数做新描述符的值。和dup2()的区别在于不会强行关闭已使用的描述符,而是寻找大于等于参数的最小未使用的值。
    F_SETFL(long)/F_GETFL(void) - 设置/获取描述符的状态,比如权限。其中,设置时,只对O_APPEND有效,权限和创建标识都无效。获取时,只能取权限和O_APPEND,创建标识取不到。
    F_SETLK/F_SETLKW/F_GETLK - 文件锁的操作
    
  2. 位与 运算用于 取某一位或者取某几位,比如取后8位:

    int a;

    取后8位: a & 0xFF

  3. 文件锁

    当多个进程同时写一个文件时,有可能引发数据混乱,这个问题需要解决。解决方案包括:进程之间的同步 或 文件锁。文件锁就是当一个进程读写文件时,对其他进程进行读写的限制。结论:一个进程读,允许其他进程读,但不允许其他进程写。一个进程写,其他进程不能读也不能写。文件锁是一个读写锁,包括读锁和写锁。

    读锁是一个共享锁,允许其他进程读(共享),不允许其他进程写(锁)。如果进程是读文件,就应该上读锁。

    写锁是一个互斥锁,不允许其他进程读/写(互斥锁)。如果进程是写文件,就应该上写锁。

    fcntl(fd,cmd,...)
    当cmd为F_SETLK/F_SETLKW时,可以对文件上锁。
    当使用文件锁时,第三个参数就是结构体flock指针。
    struct flock{
        short l_type;//锁的类型
        short l_whence;//锁定起始点的参考位置
        off_t l_start;//针对参考位置的偏移量
        off_t l_len;//锁定的区间长度
        pid_t l_pid;//只对F_GETLK有效,一般给-1
    };
    

    锁的类型包括:F_RDLCK(读锁) 、F_WRLCK(写锁)、F_UNLCK(释放锁)

    l_whence和l_start联合决定了锁定的起始点。比如:l_whence选SEEK_SET,l_start为10,就是从头开始偏移10个字节以后开始锁。

    进程结束自动释放文件锁,但最好还是程序员自己释放。

    文件锁只是内存中的一个标识,不会真正锁定文件。fcntl(F_SETLK)不能锁定read()/write(),只能锁定其他进程的加锁行为fcntl(F_SETLK)。文件锁的正确用法是:在调用read()函数之前用fcntl()加读锁,能加上再读,读完以后释放读锁;在调用write()之前用fcntl()加写锁,能加上再写,写完以后释放写锁。

    fcntl(fd,F_SETLK,&读锁);
    read(fd,...);
    fcntl(fd,F_SETLK,&释放锁);
    或:
    fcntl(fd,F_SETLK,&写锁);
    write(fd,...);
    fcntl(fd,F_SETLK,&释放锁);
    

    但不管怎么加锁,类似vi的编辑器是无法锁定。

    F_SETLK当锁加不上时,直接返回-1,而F_SETLKW当锁加不上时,会继续等待,等到能加上为止。

    F_GETLK不是获得当前的锁,而是测试一下某个锁能不能加上,并不真正的加锁。(了解)

  4. C语言中,参数可以有三种:

    传入型参数 - 给函数传值,比如: add(int,int)

    传出型参数 - 带回函数的结果,一般是指针类型

    传入传出型参数 - 先传入一个值,再带出一个值

    函数的返回值,可以用return直接返回,也可以用传出型参数返回。

  5. stat()就是用传出型参数返回文件的信息。stat()可以取得文件的以下信息:

    ls -il 的所有信息,其中最常用的是st_size。

    st_mode需要拆分,文件类型和权限。

  6. access()可以判定当前用户对文件的权限和文件是否存在。

    int access(char* fname,int mode)
    参数fname就是带路径的文件名
    mode 就是判断什么,包括:
    R_OK - 读权限
    W_OK - 写权限
    X_OK - 执行权限
    F_OK - 文件是否存在
    返回0代表有权限或者文件存在。
    
  7. 其他函数:

    • chmod() - 修改文件的权限

      chmod("a.txt",0666)

    • truncate()/ftruncate() - 指定文件的大小

      truncate("a.txt",100)

    • remove() - 删除文件/空目录

    • rename() - 文件改名

    • access() - 获取文件的权限/判断文件是否存在

    • umask() - 修改创建文件时,系统默认的权限屏蔽字。默认屏蔽其他用户的写权限 - 0002。umask()可以修改默认的权限屏蔽字(只针对新建文件)。

    • umask(mode_t)传入新的权限屏蔽字,返回之前的权限屏蔽字,用于处理之后的恢复。

    • mmap() 可以映射物理内存,但也可以映射文件,默认情况下 映射文件,映射物理内存需要加MAP_ANONYMOUS标识。

  8. 目录相关函数:

    • mkdir() - 新建一个目录

    • rmdir() - 删除一个空目录

    • chdir() - 切换当前目录 (cd)

    • getcwd() - 取当前目录(返回绝对路径) 双返回

      char* s = getcwd(0,0);

  9. 读目录的函数:

    • opendir() - 打开一个目录,返回目录流(指针)
    • readdir() - 读目录的一个子项(子目录/子文件) 效果相当于 ls 目录
    • closedir() - 关闭目录流(不写也可以)
  10. 使用递归的必要条件:

    • 使用递归以后,问题简化而不是复杂
    • 递归必须有 退出条件
    • 使用递归要注意效率问题。

四、进程管理

程序就是代码编译连接的成品(a.out),程序是硬盘上的文件。进程就是运行在内存中的程序,一个程序可以启动多次,得到多个进程。CPU(中央处理器)只能直接操作内存,不能直接操作硬盘的。硬盘上的程序要想运行,先加载到内存中去,就变成了进程。有些时候也把进程叫程序。主流的操作系统都是多进程的,每个进程内部可以用多线程实现功能的并行(同时运行)。

  1. 进程相关命令(Unix版):

  2. ps : 只能看到当前终端启动的程序

  3. ps -aux : Linux专用查看所有进程的命令,Unix不直接支持。

  4. ps -ef : 通用版

  5. kill -9 进程ID : 杀进程(发信号)

  1. 常见的管道用法:

    管道的作用就是用前面的输出作为后面的输入

  2. ​ ps -ef | wc - 统计行数、字节数等

  3. ​ ls -al | more - 分页显示(空格 回车 q)

Unix/Linux系统由 内核和SHELL,SHELL主要有: sh/bash(sh的升级版)/csh

whereis XXX 可以查看文件名XXX在哪里

  1. 父进程和子进程

    如果a进程启动了b进程,a就是父进程,b就是子进程。Unix/Linux系统的启动次序是:系统启动0进程,0进程启动进程1/进程1和进程2,其他进程都由进程1/进程1和进程2启动。

  2. 进程的状态

    每个进程都有自己的状态,主要包括:

  3. ​ S - 休眠状态,大多数进程处于休眠状态

  4. ​ s - 有子进程

  5. ​ R - 正在运行

  6. ​ Z - 僵尸进程(已经结束,但资源没有回收)

  7. ​ T - 暂停或被追踪

  1. 每个进程用进程ID(PID)做唯一标识,进程PID是系统管理。函数getpid()可以取得进程的PID。如果进程结束,PID是可以重复使用,但要延迟重用。PID唯一标识一个进程。

  2. getpid() - 取当前进程的PID

  3. getppid() - 取父进程的PID

  4. getuid() - 取当前用户的ID。

  1. 创建子进程

  2. fork() - 非常复杂的简单函数,通过复制父进程创建子进程。

  3. vfork()+execl() - 不复制任何东西,创建一个全新的子进程。

  1. 进程PID用pid_t类型,是一个非负整数。pid_t fork() , 返回子进程的PID或0,出错 -1。

  2. fork()是通过复制父进程的内存空间创建的子进程,除了代码区父子进程共享(只读),其他内存区域子进程都要复制。

  3. fork()创建子进程之后,父子进程同时运行,但谁先运行不确定,谁先结束也不确定。

  4. fork()在复制父进程的内存空间时,如果遇到文件描述符,复制描述符但不复制文件表。

  5. fork()在复制父进程的内存空间时,也会复制输出/输入缓冲区。

  6. fork()函数调用一次,返回两次。父进程返回一次,子进程也会返回一次。父进程返回子进程的PID,子进程返回0。

  1. 关于父进程的运行和资源回收:

  2. 父进程启动子进程后,父子进程同时运行。如果子进程先结束,会给父进程发信号,父进程负责回收子进程的资源。

  3. 父进程启动子进程后,父子进程同时运行。如果父进程先结束,子进程变成孤儿进程,认进程1(init)做新的父进程,init进程也叫 孤儿院。

  4. 父进程启动子进程后,父子进程同时运行。如果子进程没有给父进程发信号就结束,或者父进程没有及时处理信号,此时子进程就变成僵尸进程。

  5. fork()之前的代码父进程执行一次,fork()之后的代码父子进程分别执行一次,fork()将返回两次。

  6. 刷新输出缓冲区的条件:

  7. 遇到换行 \n

  8. 缓冲区满了

  9. 程序结束了

  10. fflush()函数人工刷新

  1. 进程结束的方式分为正常结束和非正常结束。

    正常结束包括:

    主函数中执行了return

    执行exit()

    _Exit()和_exit()

    所有线程都结束

    非正常结束:

    信号打断进程(ctrl+c、kill -9)

    最后一个线程被取消

    exit() 与 _Exit()/_exit()的区别:

    _Exit()/_exit()基本无区别,都是立即退出进程。

    exit()不是立即退出,甚至可以先执行在 atexit()中注册函数后再退出。

  2. 函数wait()/waitpid()可以让父进程等待子进程结束,并取得子进程的退出状态和退出码(return/exit(值))。

    pid_t wait(int* status)

    wait() 函数 让父进程等待任意一个子进程的结束,并返回结束子进程的PID,把结束子进程的退出状态和退出码存入status中。如果没有子进程结束,会阻塞父进程,直到有子进程结束为止。包括僵尸子进程,因此wait()也叫殓尸工。

  3. 宏函数WIFEXITED(status)判断是否正常结束,而WEXITSTATUS(status) 可以获得退出码。

  4. waitpid() 可以设置等待的方式和等待的子进程。

    pid_t waitpid(pid_t pid,
                int* status,
                int options)
    参数:pid可以指定等待哪个/哪些子进程
        status用法和wait一样
        options可以设置非阻塞的等待(不等待) options为0,没有子进程结束继续等待为WNOHANG,没有子进程结束不等待,直接返回0.
    pid的值:
        -1 : 等待任意子进程,和wait()一样
        >0 :  等待子进程的ID=pid(特指)
        0 : 等待本组子进程(与父进程相同进程组)
        <-1: 等待进程组为|pid|的所有子进程
    注:-1 和 >0 常用,后面两个了解即可。
    返回有三种可能:
        结束子进程的pid
        -1 代表出错
        0 只有在options为WNOHANG时可能返回,代表没有子进程结束,也没有出错。
    
  5. fork() 父子进程是使用相同的代码区,如果 需要父子进程代码区不同的话,可以使用 vfork()+execl()。vfork() 创建新的子进程,execl()负责提供子进程的代码和数据(程序)。execl()函数是用新的程序替换原有的程序。vfork() 从语法上和fork()完全一样,区别在于vfork()不复制任何父进程的资源。vfork()会抢父进程的资源,导致父进程阻塞。父进程解除阻塞的条件:

  6. 子进程结束时,归还父进程的资源(无并行)。

  7. 子进程调用exec系列函数(execl等),也归还父进程资源。

    注意:

vfork()确保子进程先运行(父进程没资源),调用execl()之后父子进程同时运行。

vfork()创建的子进程必须用exit()退出。

  1. execl()可以用一个新程序替换旧程序,但不新建任何的进程。如果新的程序正常启动,旧程序不再继续运行;如果新的程序启动失败,旧程序继续运行。execl(程序所在的路径,命令,选项,命令参数,NULL), 启动失败返回-1.

五、信号处理

  1. 信号(signal)

  2. 信号是Unix/Linux系统中 软件中断的最常用方式

  3. 中断:

    中断就是中止当前正在执行的代码,转而执行其他代码。

    中断分为软件中断和硬件中断。

    中断就是中止当前正在执行的代码,转而执行其他代码。

  4. 常见的信号:

    ctrl+c

    段错误

    总线错误

    整数除以0

    kill -9 发信号9

    子进程结束,给父进程发信号

  5. 信号本质就是一个非负整数,Unix和Linux在信号上有区别,Unix是48个,Linux是64个,但中间不保证连续。

  6. 每个信号都有一个 宏名称,编程时尽量使用宏名称而不是信号的值。不同的系统中,同一个宏名称对应的值可能不同。宏名称 以 SIG开头。比如:SIGINT 就是 信号2的宏名称

  7. 查看信号都有哪些,可以使用kill命令。发送信号也可以使用kill命令。

    kill -l : 查看所有信号

    kill -整数 : 发送信号

  8. 信号分为可靠信号和不可靠信号,1-31 都是 不可靠信号,34-64都是可靠信号。不可靠信号 不支持排队,因此如果有多个 相同的不可靠信号同时到来时,可能出现信号丢失。可靠信号支持排队,因此不会丢失。

  9. 信号的处理方式:

    (1) 默认处理 - 系统对每个信号都有默认处理方式,默认处理大多数都是 退出进程。

    (2) 忽略信号 - 不做任何的处理,就像没有信号一样。

    (3) 自定义信号处理函数 - 信号的处理方式改为执行我们自己定义的函数。

    注:

    信号9 不能忽略,也不能自定义处理函数。

    当前用户只能给当前用户的进程发信号,不能给其他用户的进程发信号。 root可以给所有进程发信号。

    信号0 没有特殊的意义,用于 测试是否有发信号的权限。 kill -0 3333(测试对3333进程是否有发送信号的权限)

  10. Unix系统提供了设置信号的处理方式的函数,signal()、sigaction()。

    (void(*f)(int)) signal(int signum,void(*f)(int))
    参数 signum就是被设置处理方式的信号
    第二个参数是函数指针,支持三种值:
    SIG_IGN - 代表忽略该信号
    SIG_DFL - 代表信号到来执行默认处理方式
    自定义的函数 - 代表信号到来执行自定义函数
    返回之前的信号处理方式,如果出错返回 SIG_ERR.
    
  11. 自定义信号处理方式的步骤:

    (1) 写一个处理函数,格式 void fa(int){ }

    (2) 调用signal(int signum,fa)注册处理函数。

  12. 如果父进程改变了信号的处理方式,子进程如何?

    fork()创建的子进程,与父进程的处理方式一致。

    vfork()+execl()创建的子进程,父进程忽略的,子进程也忽略;父进程默认的,子进程也默认;父进程自定义处理函数,子进程改为默认。

  13. killall可以删除所有同名的进程,比如:killall a.out 就会删除所有的a.out进程

  14. 信号的发送:

    (1) 用键盘发送信号(部分)

    ctrl+c -> 信号2

    ctrl+\ -> 信号3

    ctrl+z -> 信号20

    (2) 硬件故障/或者程序出错(部分)

    段错误、总线错误、整数除0

    (3) kill命令发送信号(全部)

    kill -信号 进程PID

    (4) 信号发送函数(全部)

    kill()、raise()、alarm()、sigqueue()等

    int kill(pid_t pid,int signum)
    参数 pid 就是发送哪个/哪些进程,使用方式和waitpid(pid)一样。
    signum就是发送哪个信号
    成功返回0,失败返回-1.
    发送信号时,一般pid 为正数,也就是发给特定进程。
    
  15. alarm()函数 - 不是真正意义的信号发送函数,而是过一段时间(秒数)发送特定的信号。

  16. sleep() - 让程序休眠一段时间(秒数),但可能被非 忽略的信号打断。

  17. usleep() - 让程序休眠一段时间(微秒).

  18. 信号集:

    多个信号可以存入信号集,类型sigset_t,可以看成一个超大型整数。 long long int - C语言的64位整数。

  19. 信号集的函数:

    (1) 增加信号和删除信号(分单独和全部)

    (2) 查询信号

    sigaddset() - 增加一个信号(二进制位 置1)

    sigdelset() - 删除一个信号(二进制位 置0)

    sigemptyset() - 全部删除信号

    sigfillset() - 填满全部信号

    sigismember() - 查询有没有某个信号

  20. 信号屏蔽:信号不确定什么时间会来,因此有可能在非常重要的场合(执行关键代码)信号到来,此时可能产生重大的错误。程序员无法阻止信号的到来,但是可以屏蔽信号,就是信号可以到来但暂时不做处理,等关键代码执行完毕,解除信号屏蔽后再做处理。信号9 屏蔽无效。

  21. 信号屏蔽/解除函数 sigprocmask()

    int sigprocmask(int how,sigset_t* set,sigset_t* old)
    参数:how就是信号屏蔽的方式,包括:
    SIG_BLOCK - 相当于旧的屏蔽+新的屏蔽
    A B C + C D E  -> A B C D E 
    SIG_UNBLOCK - 相当于旧的屏蔽 - 新的屏蔽
    A B C - C D E   -> A B 
    SIG_SETMASK - 就是无视旧的,直接替换成新的屏蔽。
    set 就是新的权限屏蔽字
    old是一个传出参数,可以传出旧的权限屏蔽字,用于恢复之前的屏蔽
    注: 信号屏蔽之后一定要解除屏蔽。
    一般情况下,how都采用SIG_SETMASK。
    
  22. 函数sigpending()可以判断在信号屏蔽期间,有没有信号来过。功能就是把信号屏蔽期间来过的信号放入信号集。

  23. sigaction()也是一个信号处理方式的注册函数,是signal()的增强版,sigaction()可以拿到更多的信号相关信息,甚至可以在发送信号的时候附带其他的数据。sigaction()中,信号的处理函数支持两种格式: signal()的格式和更复杂的格式。

  24. 信号的应用之计时器。

    每个进程在Linux中都有三种计时器,真实计时器、虚拟计时器和实用计时器。其中真实计时器是产生SIGALRM工作。计时器可以用setitimer()进行设置。

    int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);
    参数which选择哪种计时器,一般都是真实计时器。
    struct itimerval 设置计时器的开始时间和间隔时间。
    

六、进程通信

1.进程见通信

  1. 进程间通信 - IPC:

    Unix/Linux系统基于多进程,进程和进程之间经常做数据的交互,这种技术叫进程间通信。

  2. 常见的IPC:

    1 文件

    2 信号

    3 管道

    4 共享内存

    5 消息队列

    6 信号量集

    7 网络编程(socket)

    ...

  3. XSI IPC 之 共享内存、消息队列。XSI IPC 包括共享内存、消息队列和信号量集,遵循相同的规范。

  4. 标准(规范) 、产品 和 项目

    标准是行业准则,任何相关软件都必须遵守。标准是 行业共同协商的成果。做标准的公司最幸福的。

    产品就是遵循标准的软件,产品更注重质量,不是为个别客户服务的。比较轻松,不用特别赶时间

    项目是针对 特定客户的定制,客户的影响力非常大,时间一般比较紧张。比较累,而且需要年轻化

  5. XSI IPC的通用规范:(三种都可以用)

    (1) 所有的IPC结构都有一个内部的ID做唯一标识

    (2) 内部ID的获取 需要借助 外部的key,类型key_t。

    (3) key的获取有三种方式:

    a 使用宏 IPC_PRIVATE做key,但这种方式外部无法获取,因此基本不用。

    b 使用ftok()提供一个key。

    c 在头文件中统一定义所有的key。

    (4) 用key获取内部id的函数都是 xxxget(),比如: shmget() 、msgget()

    (5) 每种IPC结构都提供了一个 xxxctl()函数,这个函数的功能至少包括:

    查询、修改和删除。

    其中有一个cmd参数,值:

    IPC_STAT - 查询

    IPC_SET - 修改

    IPC_RMID - 按ID删除IPC结构

    (6) 所有IPC结构都是内核管理,不使用时需要手工删除。

    (7) IPC结构的相关命令:

    ipcs 查询当前的IPC结构

    ipcrm 删除当前的IPC结构(用id删除)

    选项: -a 所有IPC结构

    ​ -m 共享内存

    ​ -q 消息队列(更常用)

    ​ -s 信号量集

2.管道

  1. 其中,管道是最古老的的IPC之一,目前较少使用。共享内存、消息队列和信号量集 遵循相同的规范,因此编码上有很多的共同点,并且这三个统称为XSI IPC。网络编程以前用于IPC,现在更多的用于网络。

  2. 管道(pipe) - 就是用管道文件做交互媒介的IPC。管道文件是一种特殊的文件,ls时 文件类型是p。

  3. mkfifo命令/函数 管道文件名 就可以创建管道文件。touch命令和open() 都无法创建管道文件。

  4. 管道文件只是交互的媒介,不存储任何的数据;只有在有读进程 有写进程时 才能畅通,否则阻塞。

  5. 管道有两种用法: 有名管道和无名管道。

    有名管道可以用于所有进程之间的交互,而无名管道只能用于fork()创建的父子进程之间的交互。

    有名管道就是由程序员创建管道文件进行IPC。无名管道就是系统创建和维护管道文件进行IPC。

  6. 有名管道的用法:

    (1) 用mkfifo命令/函数 创建管道文件。

    (2) 像读写普通文件一样操作管道文件。

    (3) 如果不再使用管道文件,可以删除。

管道实际上是一种固定大小的缓冲区,管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。它类似于通信中半双工信道的进程通信机制,一个管道可以实现双向 的数据传输,而同一个时刻只能最多有一个方向的传输,不能两个方向同时进行。管道的容 量大小通常为内存上的一页,它的大小并不是受磁盘容量大小的限制。当管道满时,进程在写管道会被阻塞,而当管道空时,进程读管道会被阻塞。

3.共享内存

  1. 以 一块共享的物理内存做媒介。通常情况下,两个进程无法直接映射相同的内存。共享内存是效率最高的IPC。.

    共享内存的实现:

    (1) 内核先拿出一块物理内存,内核 负责管理。

    (2) 允许所有进程对这块内存进行映射。

    (3) 这样两个不同的进程 就可以 映射到 相同的物理内存上,从而实现信息的交互。

  2. 共享内存编程步骤:

    (1) 获取 key,方式ftok()或头文件定义。

    key_t ftok(char* pathname,int projectid)
    参数:pathname是一个真实存在的可访问的路径
        projectid是项目编号,低8位有效(1-255)
        返回key。
    ftok()如果给定路径有效,不会出错。会按照路径和项目ID生成一个key。相同的路径+相同的项目ID生成 相同的key。
    

    (2) 使用shmget()函数创建/获取内部ID。

    int shmget(key_t key,size_t size,int flag)
    参数:key 就是第一步返回值,外部的key
        size就是共享内存的大小
        flag在获取时用0,在新建时用:
        IPC_CREAT|0666  (权限)
        成功返回共享内存的ID,失败返回-1.
    

    (3) 使用shmat()挂接共享内存(映射)。

    void* shmat(int shmid,0,0) 可以挂接
    

    (4) 可以像正常操作一样使用共享内存。

    (5) 使用shmdt() 脱接共享内存(解除映射)。

    int shmdt(void* addr)
    adrr是shmat()的返回值,首地址(虚拟)。
    

    (6) 如果确定已经不再使用,可以使用shmctl()删除共享内存。

    
    int shmctl(int shmid, int cmd, struct shmid_ds *buf)
    参数: shmid 共享内存标识符
        cmd
            IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
            IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
            IPC_RMID:删除这片共享内存
        buf 共享内存管理结构体。具体说明参见共享内存内核结构定义部分
    

    shmctl() 可以查询、修改和删除共享内存。 查询时,会把共享内存的信息放入第三个参数 修改时,只有用户id、组id和权限可以修改。 删除时,第三个参数给0 即可。 删除共享内存时,挂接数必须为0才能真正删除,否则删除只是做一个删除标记,等挂接数为0时才真正删除。 第三个参数是结构体指针:struct shmid_ds

4.消息队列

  1. 共享内存虽然速度最快,但当多个进程同时写数据时,会发生互相覆盖,导致数据混乱。消息队列就可以解决多个进程同时写数据的问题。

  2. 消息队列就是 存放消息的队列。队列是线性的数据结构,先入先出(FIFO)。一般情况下,队列有满有空。数据先封入消息中,然后再把消息存入队列。

  3. 消息队列的编程步骤:

    (1) 得到外部的key,函数ftok()。

    (2) 用key创建/获取一个队列(消息队列),函数msgget()。

    msgget() msgctl()与共享内存的函数相似。
    int msgsnd(int msgid,void* msgp,size_t size,int flag)
    参数 msgid就是消息队列的ID
        msgp就是消息的首地址,其中消息分为有类型消息和无类型消息,更规范的是 有类型消息。有类型消息就是一个结构:
            struct Msg{ //结构的命令可以自定义
            long mtype; //消息的类型,必须大于0
            ... //数据区域,可以任意写
            };
            将来在接收消息时,可以按照类型有选择的接收消息。
        size参数是 数据区域的大小,不包括mtype(有些时候包括了也行)。
        flag 就是选项,可以是0 代表阻塞(队列满了等待),也可以是IPC_NOWAIT 代表非阻塞(队列满了直接返回错误)。
        成功返回0,失败返回-1.
    

    (3) 把数据/消息 存入队列 或 从队列中取出。函数 msgsnd()存入/msgrcv()取出

    int msgrcv(int msgid,void* msgp,size_t size,long mtype,int flag)
    前三个参数和msgsnd()一样。
    参数flag和msgsnd()也一样。
    参数mtype可以让接收者有选择的接收消息,值可能是:
    0 - 接收任意类型的消息(第一个,先入先出)
    >0  - 接收 类型为mtype的特定消息
    <0  - 接收类型 小于等于 mtype绝对值的消息,从小到大接收。
    成功返回接收到的数据大小,失败返回-1.
    

    (4) 如果不再使用消息队列,可以msgctl()删除。

5.信号量集

  1. 信号量是一个计数器,用于控制访问共享资源的最大并行进程数。信号量集就是信号量的数组。

  2. 信号量的工作方式:设定一个初始计数,每来一个进程计数-1,每完成一个进程计数+1,计数到0不允许进程访问,直到大于0为止。

  3. 信号量集的编程步骤:

    (1) 获得key。

    (2) 用semget()获取信号量集的ID。

    (3) 用semctl()设置信号量的初始计数。

    semctl()设置初始值的代码:
    semctl(semid,index,SETVAL,count)
    其中,semid是信号量集的ID,index是信号量在信号量集中的下标,SETVAL是宏,count就是该信号量的初始计数。
    

    (4) 用semop()进行加1或减1的操作。

    int semop(int semid, struct sembuf semoparray[],size_t nops);
    参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
    struct sembuf{
        unsigned short sem_num;//操作信号量的下标
        short sem_op; //对信号量操作方式。 -1 和 1
        short sem_flg; //0 等待  IPC_NOWAIT不等
    };
    

    (5) 如果不再使用,用semctl()删除。

七、网络通信

  1. 网络编程(非常重要),网络常识 - 比如: 协议、IP地址、端口等。

    网络编程的步骤和函数

    基于TCP/UDP的开发

  2. OSI 七层模型: 物理层、数据链路层、网络层、传输层、会话层、表现层、应用层

  3. 协议就是 计算机信息交互时的规范。常见的协议: http协议 - 超文本传输协议

    ftp协议 - 文件传输协议

    tcp协议 - 传输控制协议 (传输层)

    udp协议 - 用户数据报协议(传输层)

    ip 协议 - 网络层协议

    ...

  4. 协议簇就是多个协议的集合,协议簇一般都是以核心协议命名。也有非官方的写成协议族。

  5. IP地址就是网络中计算机的唯一标识,本质是一个整数。IP地址早期都是32位的,叫IPV4。后来推出了IPV6(128位)。主流还是IPV4。IP地址有两种描述方式: 点分十进制和十六进制。

    点分十进制就是每8位做一个整数(0-255),分4段,中间用点. 隔开。

    十六进制就是把32位二进制直接写成 8位十六进制。

    计算机更倾向十六进制,而人更习惯点分十进制。

  6. IP地址其实绑定的是网卡,每个网卡在出厂时都有一个唯一的物理地址(MAC地址),IP地址其实是找到网卡的物理地址,找到网卡从而找到计算机。

  7. IP地址分为A/B/C/D 四类。系统预留了127.0.0.1,做本机的IP地址。

  8. 关于网络的一些基本命令:

    ipconfig - windows dos命令,查看IP地址

    ifconfig - Unix/Linux 查看IP地址。

    ping IP地址 - 测试IP地址是否可以访问。

  9. 子网掩码 就是用来 判断IP是否同一网段。

    IP地址:166.111.160.1
           166.111.161.45 
    子网掩码:255.255.254.0
    做位与运算:
    166.111.160.1 - 所有偶数 最后一位0,奇数1
    255.255.254.0
    \------------------- 
    166.111.160.0
    166.111.161.45
    255.255.254.0
    \-------------------
    166.111.160.0    ---> 结果相同,同一网段
    
  10. IP地址可以让我们找到计算机,但没有找到对应的进程。在网络中,端口代表计算机内部的一个进程。有IP地址+端口号 就可以网络通信。ip地址+端口号的网络编程就是socket编程。

  11. 端口号也是一个整数short,0到65535。其中:0-1023不要用,系统占用其中很多端口。48000以后也不要用,不稳定,系统随时可能征用。有些软件会强占一些端口,这些端口不要使用。Oracle数据库 占1521端口、8080端口等。常用端口:

    Http端口 80

    ftp端口 21

    telnet端口 23

  12. 字节顺序: 整数是4个字节,有些计算机从低位字节到高位字节存储,有些机器从高位字节到低位字节存储。本机的字节顺序无法确定,但网络的字节顺序是固定的。编程用网络字节顺序传输,到本地以后再网络转本地格式。

  13. Unix/Linux网络编程的实现,有固定套路并且有一些不方便的函数、结构。socket编程 (插座、套接字)。socket通信包括 一对一 和一对多。先研究一对一(一对多的模式也一样)。socket编程 早期用来做进程间通信(IPC),现在主体是网络,编程的代码差不多。

  14. 本地通信(IPC)

    • 服务器端的编程步骤:

      (1) 创建一个socket,使用socket()

      int socket(int domain,int type,int protocol)
      参数:domain叫域,用于选择协议簇
      AF_UNIX/AF_LOCAL/AF_FILE : 本地通信IPC
      AF_INET  : 网络通信
      AF_INET6: IPV6的网络通信
      其中,AF换成PF效果一样。
      type 选择通信的类型(选协议)
      SOCK_STREAM  :  数据流(TCP协议)
      SOCK_DGRAM  : 数据报(UDP协议)
      protocol 本来应该选择协议,但实际上没什么用,协议已经被前2个参数决定,给0即可。
      成功返回 socket描述符,类似文件描述符。失败返回-1.
      注:读写函数 可以操作socket描述符。
      

      (2)准备通信地址(IPC是文件,网络是IP/端口),系统提供了三种通信地址,就是三个结构体。

      (1) struct sockaddr本身不存数据,做函数的参数。
      (2) struct sockaddr_un 存本地通信的通信地址
      (3) struct sockaddr_in 存网络通信的通信地址
      
      #include <sys/un.h>(本地)
          struct sockaddr_un{
          int sun_family; //协议簇,与socket()一致
          char sun_path[];//socket文件的路径
      };
      
      #include <netinet/in.h>(网络)
      struct sockaddr_in{
          int sin_family; //协议簇,与socket()一致
          short sin_port; //端口号
          struct in_addr sin_addr; // IP地址
      };
      

      (3) 绑定socket描述符和通信地址

      bind(int sockfd,struct sockaddr* addr,int length)
      length是通信地址的sizeof
      

      (4) 通信(read()、write())

      (5) 关闭socket描述符(close())

    • 客户端的编程步骤和服务器端编程步骤一样,除了第三步把bind换成connect(),但函数的参数不用改变。bind() 是服务器绑定通信地址,开放端口。connect()是客户端连接服务器,通信地址要使用服务器的。 本地通信 媒介是 socket文件,类型s。

  15. 在使用网络编程时,IP地址和端口号都需要做一些处理。IP地址需要做点分十进制和十六进制转换,使用函数 inet_addr();端口号需要本机格式和网络格式之间的转换,使用函数htons()。

  16. 有两种socket描述符,其中一种负责 等待客户端的连接,当有客户端连接时,启动一个新的描述符负责信息交互。

  17. TCP协议是一个基于连接(有连接)的协议,全程保持客户端和服务器的连接。会重发一切的错误数据,因此TCP可以保证数据的完整和有效。缺点就是当客户端超级多的时候,效率非常低。

  18. UDP协议是一个不基于连接(无连接)的协议,发送数据时连接一下,发送完了就断开,而且不考虑是否接收到了。UDP效率比TCP高,UDP不保证数据的有效和完整。QQ/MSN 都是采用UDP协议。

  19. TCP一对多的编程步骤:

    • 服务器端:

      1 socket(),得到第一类的socket描述符。 2 准备通信地址 struct sockaddr_in 3 绑定 bind(),开发断开。 4 监听客户端的连接,函数listen()。

      int listen(int sockfd,int backlog)
      设置当多个客户端同时 连接时,需要把多余的客户端存入队列,backlog就是队列的最大长度。
      

      5 等待客户端的连接,函数accept(),返回新的socket描述符,用于信息交互。(无客户端连接会阻塞).

      int accept(int sockfd,struct sockaddr*  addr, socklen_t* len)
      参数 sockfd就是socket描述符,第一步的返回
      addr是一个结构体指针,存客户端的通信地址
      len是传入传出参数,先传入addr的长度,再传出接收到的客户端通信地址的真实长度。
      返回 新的socket描述符,失败返回 -1.
      

      6 用第五步返回描述符进行读写操作。 7 close()关闭两个描述符。

    • 客户端:

      1 socket(),得到第一类的socket描述符。 2 准备通信地址 struct sockaddr_in 3 连接 connect()。

      4 描述符进行读写操作read() write()

      5 关闭close()描述符。

  20. UDP编程: TCP和UDP的区分主要在于socket()第二个参数,如果是SOCK_STREAM就是TCP,SOCK_DGRAM就是UDP。UDP分 发送方和接收方。UDP发送数据很少使用write(),使用sendto()。接收数据可以使用两个函数:read()/recvfrom()。区别就是read()函数不知道数据的来源,而recvfrom()可以获取数据的发送者信息。

    ssize_t sendto(int sockfd,void* data,size_t length,int flags, struct sockaddr* addr,socklen_t size)
    参数:前三个 参数与write()一样,flags一般给0即可,addr就是第二步的通信地址的指针,size就是sizeof(addr)。   
    成功返回发送的字节数,失败返回-1.
    注:sendto()好像write()和connect()的合体。
    
    ssize_t recvfrom(int sockfd,void* data,size_t length,int flags, struct sockaddr* addr,socklen_t* size)
    参数:前三个和read()一样,flags给0即可,addr是用于存储发送者通信地址的指针,size是一个传入传出参数,把addr的sizeof传入,再传出真正获取到的通信地址的大小。(后两个参数和accept()一样)
    成功返回接收到的字节数,失败返回 -1.
    注:recvfrom像 read() 和 accept()的合体。
    
  21. 网络编程总结:

    • TCP的编程步骤:

      • Server端:

        1 socket()

        2 准备通信地址 struct sockaddr_in

        3 绑定 bind()

        4 listen()

        5 accept(),返回一个用于交互的新的描述符

        6 读写 read() write()

        7 关闭close()

      • Client端:

        1 socket()

        2 准备通信地址 struct sockaddr_in

        3 连接 connect()

        4 读写 read() write()

        5 关闭close()

    • UDP的编程步骤:

      • Server端:

        1 socket()

        2 准备通信地址,struct sockaddr_in

        3 绑定 bind()

        4 发送或接收 sendto() recvfrom() read()

        5 关闭close()

      • Client端:

        1 socket()

        2 准备通信地址,struct sockaddr_in

        3 发送或接收 sendto() recvfrom() read()

        4 关闭close()

八、线程管理

线程(thread) - 做应用,有网络必有线程。主流的操作系统都是支持多进程的,每个进程的内部可以启动多线程完成代码的并行;每个线程的内部可以无限启动多线程。线程是轻量级的代码并行,不需要额外创建过多的内存空间,而是共享所在进程的内存空间。线程只需要额外建一个独立的栈即可。多线程之间互相独立,又互相影响。 多线程可以大幅提升代码的效率。程序的运行必须拥有CPU和内存,内存可分, CPU不可分,如何实现并行。大多数的操作系统都是采用CPU时间片实现CPU的在多线程之间的轮换。CPU时间片是极短的一段CPU的执行时间,拥有CPU时间片的线程有机会运行。比如:人的感官是需要时间的,比如视觉0.1秒。就是100毫秒。假定CPU时间片是1毫秒,有4个线程。每个线程先分一个时间片,也就是1毫秒的CPU运行时间。每个线程只能运行1毫秒,时间片的运行时间到了以后就只能看其他线程运行,直到所有线程的时间片都运行完毕,再重新分配。针对时间点的并行是不存在的,针对时间段的并行就是我们通常说的代码并行。每个进程都有一个主线程,就是main()函数,主线程结束,进程也结束,同时导致所有线程都结束。

  1. 线程的编程:

    Unix/Linux的线程相关函数都在pthread.h中,代码都在libpthread.so中。线程相关的函数/结构都以pthread_ 开头。比如创建线程函数:

    (1). pthread_create();

    int pthread_create( pthread_t* id,
                        pthread_attr_t* attr, 
                        void* (*fa)(void*),
                        void* arg )
    

    pthread_create()是一个四针函数,参数id就是用于存储线程ID的;

    attr是线程的属性,一般给0即可(默认属性);

    fa是一个函数指针,写线程执行的代码;

    arg是传给fa的参数。fa+arg指定了线程要执行的代码。

    返回值: 成功返回0,失败返回错误码,想看错误信息需要用strerror()做转换。

    每个线程启动以后,只能执行一个函数,主线程执行的是main(),其他线程执行自定义的一个函数。这个函数以并行的方式运行。线程之间的代码乱序执行,每个线程的内部代码都是顺序执行。每个线程都会返回自己的错误码,而不是使用errno。

    (2). pthread_join();

    pthread_join()函数可以让一个线程等待另外一个线程的结束,并取得线程的返回值。如果在线程a中调用了pthread_join(b,0),线程a就会等待线程b的结束,等线程b结束以后a才能继续运行。线程传参时,一定要注意保证地址的有效性,尤其是堆内存。支持直接传递int。

  2. 函数的返回

    (1). 能返回局部变量,但不能返回指向局部变量的指针。

    (2). static的局部变量的地址可以返回(全局区)。

    (3). 数组理论上可以做返回值,但返回值类型不能写数组。最好用指针。

    int[] get() 错.
    void fa(int* pi){ *pi = 200;}
    int main(){
        int x;
        fa(&x); -> pi = &x ->*pi = x = 200;
    }
    
    void fa(int** pi){ *pi = 地址1;}
    int main(){
        int* px;
        fa(&px); ->pi = &px ->*pi = px = 地址1;
    }
    
  3. 线程的状态:

    线程应该处于以下两种状态:

    分离状态:就是线程一旦结束,不用管其他线程,直接回收资源。函数pthread_detach()设置线程分离状态。

    join状态:如果线程用pthread_join(),就处于join()状态,就是线程结束时暂不回收资源,到pthread_join()函数结束时再回收资源。

    注:没有分离也没有join()的线程资源回收是没有保障的。分离状态的线程再调用pthread_join()没有效果

  4. 线程的退出

    正常退出:

    在线程的函数中执行了return语句。

    执行了pthread_exit(void*)函数

    非正常退出:

    自身出现错误

    被其他线程终止/取消

    exit()和pthread_exit(void*)的区别?

    exit() 是结束进程,所有线程全结束

    pthread_exit()是结束线程,其他线程继续运行

    参数void* 和 return 一样,都是 用于返回值。

    取消线程的函数: pthread_cancel()

九、线程同步

多线程之间是共享进程的资源,因此有可能出现共享数据的冲突,解决方案就是把并行访问改为串行访问,这种技术叫线程同步。线程同步的技术包括: 互斥量、信号量、条件变量。

  1. 互斥量又叫互斥锁,是线程在设计时官方的同步技术,编程步骤如下:

    (1) 声明互斥量

    ​ pthread_mutex_t lock; //变量名不一定叫lock

    (2) 初始化互斥量

    ​ pthread_mutex_init(&lock); 或在声明的同时赋值: pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

    (3) 加锁/上锁 pthread_mutex_lock(&lock);

    (4) 执行共享数据的访问代码

    (5) 解锁 pthread_mutex_unlock(&lock);

    (6) 释放锁的资源

    ​ pthread_mutex_destroy(&lock);

  2. 信号量是一个计数器,用于控制访问共享资源的最大的并行 进程/线程的数量。

    信号量的工作原理:先设置最大值最初始计数,每上来一个计数减1,每退出一个就加1,到0就不允许新进程/线程访问,除非计数又回到大于0。 信号量不属于线程的范围,不在pthread.h中,只是一个线程计数辅助。头文件 semaphore.h 信号量如果初始计数为1,效果等同于互斥量。

    信号量的编程步骤:

    (1) 声明信号量 sem_t sem;

    (2) 初始化信号量的原始计数 sem_init()

       sem_init(&sem,0,count)
    
       第一个参数就是信号量的地址
    
       第二个参数必须是0,0代表线程的计数,非0代表进程的计数(Linux系统没有提供进程计数功能)。
    
       第三个参数就是 计数的初始值(最大计数)。
    

    (3) 计数减1 sem_wait(&sem);

    (4) 正常使用

    (5) 计数加1 sem_post(&sem);

    (6) 释放信号量资源 sem_destroy(&sem);

    使用线程同步技术,小心避免死锁。

    pthread_mutex_t lock1,lock2;
    线程a:
    lock(&lock1);
    ...
    lock(&lock2); //等待线程b unlock(&lock2)
    ...
    unlock(&lock2);
    unlock(&lock1);  
    线程b:
    lock(&lock2);
    ...
    lock(&lock1);//等待线程a unlock(&lock1)
    ...
    unlock(&lock1);
    unlock(&lock2);
    

    结果就是看起来都问题,但执行 a和b互相锁定,死锁。


打赏

微信支付 支付宝支付

License

本作品由Simonhttp://www.uusystem.com)创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 欢迎转载,但任何转载必须保留完整文章,在显要地方显示此声明以及原文链接。如您有任何疑问或者授权方面的协商,请邮件:postmaster@uusystem.com。

results matching ""

    No results matching ""