2017年1月

linux系统编程

文件与I/O函数

  1. C标准I/O库函数与Unbuffered I/O函数
  • C标准I/O库函数:fwrite, fgetc, fopen, ...
  • 对于C标准I/O库来说,打开的文件由FILE *指针标识,而对于内核来说,打开的文件由文件描述符标识,文件描述符从open系统调用获得,在使用read、write、close系统调用时都需要传文件描述符
  • open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O)函数, 在头文件unistd.h中声明
  • 网络编程通常直接调用Unbuffered I/O函数
  • 每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符, task_struct中有一个指针指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针
  • 用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符
  1. open函数与fopen函数的轻微区别:
  • 以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
  • 以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写
  • open函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
// 返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
  1. 阻塞
  • 阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行
  1. 程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。在C标准库中分别用FILE *指针stdin、stdout和stderr表示
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
  1. 非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"

int main(void)
{
    char buf[10];
    int fd, n, i;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);// 为什么不直接对STDIN_FILENO做非阻塞read? 因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志
    if(fd<0) {
        perror("open /dev/tty");
        exit(1);
    }
    for(i=0; i<5; i++) {
        n = read(fd, buf, 10);
        if(n>=0)
            break;
        if(errno!=EAGAIN) {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(1);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    }
    if(i==5)
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    else
        write(STDOUT_FILENO, buf, n);
    close(fd);
    return 0;
}
  1. fcntl
  • fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件
  • 文件描述符和shell重定向
./a.out 5 5<>temp.foo 
# Shell在执行a.out时在它的文件描述符5上打开文件temp.foo,并且是可读可写的
# 如果在<、>、>>、<>前面添一个数字,该数字就表示在哪个文件描述符上打开文件,例如2>>temp.foo表示将标准错误输出重定向到文件temp.foo并且以追加方式写入文件,注意2和>>之间不能有空格,否则2就被解释成命令行参数了。文件描述符数字还可以出现在重定向符号右边, 文件描述符数字写在重定向符号右边需要加&号,否则就被解释成文件名了,2>&1其中的>左右两边都不能有空格
# 例如: command > /dev/null 2>&1 标准输出为/dev/null, 2>&1 , 表示把标准错误输出也重定向到标准输出即/dev/null 
  • fcntl改变文件状态为非阻塞
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL);
    flags |= O_NONBLOCK;
    if(fcntl(STDIN_FILENO, F_SETFL, flags)==-1) {
        perror("fcntl");
        exit(1);
    }
  • fcntl读取文件状态
int val;
val = fcntl(STDIN_FILENO, F_GETFL);
switch(val & O_ACCMODE) {// 用掩码O_ACCMODE取出它的读写位
    case O_RDONLY:
    break;
    case O_WRONLY:
    break;
    case O_RDWR:
    break;
    default:
    // invalid access mode 
}
if (val & O_APPEND)  
    printf("append");
if (val & O_NONBLOCK)           
    printf("nonblocking");

VFS 虚拟文件系统

dup和dup2函数

  • 复制一个现存的文件描述符, 使两个文件描述符指向同一个file结构体
  • 成功返回新分配或指定的文件描述符, 如果出错则返回-1
  • dup2可以用newfd指定新描述符的数值
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);

进程

打印环境变量

#include <stdio.h>

int main(void)
{
    extern char **environ;
        // libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
    int i;
    for(i=0; environ[i]!=NULL; i++)
        printf("%s\n", environ[i]);
    return 0;
}

进程控制

  • fork函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main (int argc , char * argv[]) {
    // fork
    pid_t pid;
    char * message;
    int n;
    pid = fork();
    if ( pid < 0 ) { 
        perror("fork failed");
        exit(1);
    }   
    if ( pid == 0 ) { 
        message = "This is the child\n";
        n = 6;
    } else {
        message = "This is the parent\n";
        n = 3;
    }   
    
    for (; n > 0; n--) {
        printf("%s",message);
        sleep(1);
    }   
    return 0;
}
/*
1.fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id
2.fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法
*/
  • wait和waitpid函数
    • 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程
    • 如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了
    • 如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它
#include <unistd.h>
#include <stdlib.h>
// 僵尸进程例子, 子进程结束后, 父进程既没有结束没有调用wait或waitpid清理子进程, 子进程此时就是僵尸进程
int main(void)
{
    pid_t pid=fork();
    if(pid<0) {
        perror("fork");
        exit(1);
    }
    if(pid>0) { /* parent */
        while(1);
    }
    /* child */
    return 0;     
}
  • wait和waitpid函数解析
    • 若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会以下几种情况
      • 阻塞(如果它的所有子进程都还在运行)
      • 带子进程的终止信息立即返回(如果一个子进程已终止,等待父进程读取它的终止信息)
      • 出错返回-1(没有子进程)
    • 区别:
      • 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0
      • wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程
    • 通过宏定义读取status
      • WIFEXITED(stat_val), 子进程正常退出返回非0
      • WEXITSTATUS(stat_val), 获得子进程exit()返回的结束代码,一般先要用WIFEXITED判断
      • WIFSIGNALED(stat_val), 如果子进程是因为信号而结束则此宏值返回非0
      • WTERMSIG(stat_val), 取出的字段值就是信号的编号, 一般先要用WIFSIGNALED判断
      • WIFSTOPPED(stat_val), 子进程接收到停止信号时返回非0
      • WSTOPSIG(stat_val), 返回导致子进程停止的信号类型, 一般先要用WIFSTOPPED判断
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status); // 如果参数status不是空指针,则子进程的终止信息通过这个参数传出
pid_t waitpid(pid_t pid, int *status, int options);

进程间通信(IPC)

  • 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区, 两个进程在这个缓存区上交换数据, 内核提供的这种机制称为进程间通信

  • 管道(pipe)

    • 4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
      • 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样
      • 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回
      • 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
      • 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回
#include <unistd.h>
int pipe(int filedes[2]);
/*
调用pipe函数成功即在内核中开辟一块缓存区, 失败返回-1
通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端
*/
  • 例子:
#include <stdlib.h>
#include <unistd.h>
#define MAXLINE 80

int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if (pipe(fd) < 0) {
        perror("pipe");
        exit(1);
    }
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid > 0) { /* parent */
        close(fd[0]);// 不使用的读端或写端必须关闭,如果不关闭会有什么问题???
        write(fd[1], "hello world\n", 12);
        wait(NULL);
    } else {       /* child */
        close(fd[1]);
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }
    return 0;
}
  • 文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道
    • FIFO(有名管道)
      • 有名管道(FIFO)的创建可以使用 mkfifo() 函数,该函数类似文件中的open() 操作,可以指定管道的路径和访问权限 (用户也可以在命令行使用 “mknod <管道名>”来创建有名管道)
      • 在创建管道成功以后,就可以使用open()、read() 和 write() 这些函数了。与普通文件一样,对于为读而打开的管道可在 open() 中设置 O_RDONLY,对于为写而打开的管道可在 open() 中设置O_WRONLY。
      • 不支持lseek
    • Unix Domain Socket
    • 几个进程可以在文件系统中读写某个共享文件,也可以通过给文件加锁来实现进程间同步

信号

  • kill -l:查看系统定义的信号列表

  • man 7 signal:解析每个信号的宏定义名称|编号|默认处理动|简要介绍

    • 默认处动作:term(终止当前进程) ; core(终止当前进程并且Core Dump) ; ign(忽略该信号) ; stop(停止当前进程) ; Cont(继续执行先前停止的进程)
    • 什么是core dump?
      • 当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump
      • 事后可以用调试器检查core文件以查清错误原因
      • 默认是不允许产生core文件的(通过ulimit命令设置core file size)
  • 产生信号的条件:

    • 用户在终端按下某些键时,终端驱动程序会发送信号给前台进程, 例如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号
    • 硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号, 例如当前进程执行了除以0的指令;再比如当前进程访问了非法内存地址
    • 进程调用kill(2)函数
    • 可以用kill(1)命令发送信号给某个进程, 也是调用kill(2)实现的
    • 当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号
  • 用户程序可以调用sigaction(2)函数告诉内核如何处理某种信号(不想按默认动作处理信号)

    • 可选处理动作有三种:
      • 忽略此信号
      • 执行该信号的默认处理动作
      • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号
  • 调用系统函数向进程发信号

    • kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号),abort函数使当前进程接收到SIGABRT信号而异常终止
#include <signal.h>
#include <stdlib.h>
// 成功返回0,错误返回-1
int kill(pid_t pid, int signo);
int raise(int signo);
void abort(void);
  • 由软件条件产生信号 alarm函数和SIGALRM信号
    • 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
阻塞信号
  • 执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

  • 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集

  • 阻塞信号集也叫做当前进程的信号屏蔽字

  • 信号集操作函数

#include <signal.h>

int sigemptyset(sigset_t *set); // 初始化set所指向的信号集, 使其中所有信号的对应bit清零
int sigfillset(sigset_t *set);// 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位
int sigaddset(sigset_t *set, int signo);// 在该信号集中添加某种有效信号
int sigdelset(sigset_t *set, int signo);// 在该信号集中删除某种有效信号
int sigismember(const sigset_t *set, int signo);// 判断一个信号集的有效信号中是否包含某种信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);// 读取或更改进程的信号屏蔽字
int sigpending(sigset_t *set);// 读取当前进程的未决信号集,通过set参数传出
  • 信号操作实验
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void printsigset(const sigset_t *set) {
    int i;
    for (i = 1;i < 32;i++) {
        // 判断一个信号集的有效信号中是否包含某种信号
        if (sigismember(set,i) == 1) {
            // putchar('1');
            write(STDOUT_FILENO,"1",1);
        } else {
            // putchar('0');
            write(STDOUT_FILENO,"0",1);
        }   
    }   
    puts("");
}

int main(void) {
    sigset_t s,p;// 信号集
    sigemptyset(&s); // sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零
    sigaddset(&s,SIGINT);// 向信号集中添加某种有效信号
    sigprocmask(SIG_BLOCK,&s,NULL);// 把set中的信号添加到信号屏蔽字(阻塞信号集)
    while (1) {
        sigpending(&p); // 读取当前进程的未决信号集
        printsigset(&p);
        sleep(1);
    }   
    return 0;
}
信号捕捉
  • sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
/*
act, oact指向的sigaction结构体
struct sigaction {
   void      (*sa_handler)(int);   /* addr of signal handler, */
                                       /* or SIG_IGN, or SIG_DFL */
   sigset_t sa_mask;               /* additional signals to block */
   int      sa_flags;              /* signal options, Figure 10.16 */

   /* alternate handler */
   void     (*sa_sigaction)(int, siginfo_t *, void *);
};
*/
  • alarm和pause实现sleep(3)函数
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    unsigned int unslept;

    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    alarm(nsecs);
    pause();

    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL);

    return unslept;
}

int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }
    return 0;
}
  • 竞态条件与sigsuspend函数...

终端

  • 每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件

  • 通过Ctrl-Alt-F1~Ctrl-Alt-F6切换到6个字符终端,相当于有6套虚拟的终端设备,它们共用同一套物理终端设备,对应的设备文件分别是/dev/tty1~/dev/tty6,所以称为虚拟终端

  • 内核中处理终端设备的模块包括硬件驱动程序和线路规程

    • 硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理
  • 伪终端(Pseudo TTY):一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程; 从设备和/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备

  • 作业控制:

    • 个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制
    • Session与进程组
    • stty tostop:stty命令设置终端选项,禁止后台进程写,然后启动一个后台进程准备往终端写,这时进程收到一个SIGTTOU信号,默认处理动作也是停止进程
  • 守护进程

// 创建守护进程, daemonize(3)函数同样实现
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

void daemonize(void)
{
    pid_t  pid;

    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    } else if (pid != 0) /* parent */
        exit(0);
    setsid();// 关键!! 

    /*
     * Change the current working directory to the root.
     */
    if (chdir("/") < 0) {
        perror("chdir");
        exit(1);
    } 

    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    close(0);
    open("/dev/null", O_RDWR);
    dup2(0, 1);
    dup2(0, 2);
}

int main(void)
{
    daemonize();
    while(1);
}

线程

  • 创建线程的函数pthread_create
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
    const pthread_attr_t *restrict attr,
    void *(*start_routine)(void*), void *restrict arg);
// 成功返回0, 失败返回错误号
// 在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定,start_routine返回时,这个线程就退出了
// pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元
// start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *
// 其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态
  • 线程创建列子 :
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_t ntid;

void printids(const char *s)
{
    pid_t      pid;
    pthread_t  tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
           (unsigned int)tid, (unsigned int)tid);
}

void *thr_fn(void *arg)
{
    printids(arg);
    return NULL;
}

int main(void)
{
    int err;

    err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
    if (err != 0) {
        fprintf(stderr, "can't create thread: %s\n", strerror(err));// 由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印
        exit(1);
    }
    printids("main thread:");
    sleep(1);
        // 如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒

    return 0;
}
  • 终止线程:
    • 线程函数中return(主线程不行,相当于调用了exit)
    • 一个线程可以调用pthread_cancel终止同一进程中的另一个线程
    • 线程可以调用pthread_exit终止自己
#include <pthread.h>
void pthread_exit(void *value_ptr);
// value_ptr是void *类型,其它线程可以调用pthread_join获得这个指针
// pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的

#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
// 调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下
// 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
// 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
// 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
  • 例子:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void * thr_fn1(void * arg) {
    printf("thread 1 returning\n");
    return (void *)1;// 线程结束方式1:非主线程返回
}

void * thr_fn2(void * arg) {
    printf("thread 2 exiting\n");
    pthread_exit((void *)2); // 线程结束方式2:pthread_exit
}

void * thr_fn3(void * arg) {
    while (1) {
        printf("thread 3 running\n");// 线程结束方式3:pthread_cancel
        sleep(1);
    }   
}

int main(void) {
    pthread_t tid;
    void *tret;
    
    pthread_create(&tid,NULL,thr_fn1,NULL);
    pthread_join(tid,&tret);
    printf("thread 1 exit code %d\n",(int)tret);
    
    pthread_create(&tid,NULL,thr_fn2,NULL);
    pthread_join(tid,&tret);
    printf("thread 2 exit code %d\n",(int)tret);
    
    pthread_create(&tid,NULL,thr_fn3,NULL);
    pthread_cancel(tid);
    // 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止
    // 但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态
    // 调用pthread_detach之后不能调用pthread_join,反之亦然
    pthread_join(tid,&tret);
    printf("thread 3 cancel code %d\n",(int)tret);
        
    return 0;
}

线程间同步

  • 对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock)
  • Mutex用pthread_mutex_t类型的变量表示
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex); // 销毁锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
       const pthread_mutexattr_t *restrict attr); // 初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL
  • Mutex加锁和解锁操作:
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);// 获取锁(阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex);// 获取锁(非阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex);// 释放锁
  • Condition Variable:阻塞等待条件
  • 创建和销毁:
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 阻塞和通知:
#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);// 阻塞直到条件成立
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);// 阻塞直到条件成立或超时
int pthread_cond_broadcast(pthread_cond_t *cond);// 唤醒所有等待的进程
int pthread_cond_signal(pthread_cond_t *cond);// 唤醒某个等待的进程
  • 列子:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>

// 生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取走结构体
struct msg {
    struct msg * next;
    int num;
};

struct msg * head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void * consumer (void * arg) {
    struct msg * mp; 
    while (1) {
        pthread_mutex_lock(&lock);
        while (head == NULL) {
            pthread_cond_wait(&has_product,&lock);
        }   
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&lock);
        printf("Consume %d\n",mp->num);
        free(mp);
        sleep(rand() % 5); 
    }   
} 

void * producer (void * arg) {
    struct msg * mp; 
    
    while (1) {
        mp = malloc(sizeof(struct msg));
        pthread_mutex_lock(&lock);
        mp->next = head;
        mp->num = rand() % 1000;
        head = mp; 
        printf("Product %d\n",mp->num);
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&has_product);
        sleep(rand() % 5); 
    }   
}

int main (int argc,char * argv[]) {
    pthread_t pid,cid;
    
    srand(time(NULL));
    pthread_create(&pid,NULL,producer,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    return 0;
}
~ 
  • Semaphore信号量
    • 详见sem_overview(7),这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
// semaphore变量的类型为sem_t, pshared参数为0表示信号量用于同一进程的线程间同步, value值表示可以资源的个数
int sem_wait(sem_t *sem);
// 调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
// sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程
int sem_destroy(sem_t * sem);
// 用完semaphore变量之后应该调用sem_destroy()释放与semaphore相关的资源
  • 例子:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];
sem_t blank_number,product_number;

void * producter (void * arg) {
    static int p = 0;

    while (1) {
        sem_wait(&blank_number);// 5个空位置, 用完就阻塞
        queue[p] = rand() % 1000;
        printf("Produce %d \n",queue[p]);
        p = (p+1) % NUM;
        sleep(rand()%5);
        sem_post(&product_number);
    }   
}

void * consumer (void * arg) {
    static int c = 0;
    while (1) {
        sem_wait(&product_number);
        printf("Consume %d\n",queue[c]);
        c = (c+1) % NUM;
        sleep(rand()%5);
        sem_post(&blank_number);// 腾出空位置
    }   
}

int main (int argc,char * argv[]) {
    pthread_t pid,cid;
    
    sem_init(&blank_number,0,NUM);
    sem_init(&product_number,0,0);
    
    pthread_create(&pid,NULL,producter,NULL);
    pthread_create(&cid,NULL,consumer,NULL);
    
    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    
    sem_destroy(&blank_number);
    sem_destroy(&product_number);

    return 0;
}

TCP/IP

  • TCP/IP数据包的封装

  • 以太网帧格式

  • ARP数据报格式

  • IP数据报格式

  • UDP段格式

  • TCP段格式

  • TCP连接建立断开

    • TCP建立链接过程:
      • 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
      • 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
      • 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。
    • 数据传输过程:
      • 客户端发出段4,包含从序号1001开始的20个字节数据。
      • 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
      • 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
    • 关闭链接过程:
      • 客户端发出段7,FIN位表示关闭连接的请求。
      • 服务器发出段8,应答客户端的关闭连接请求。
      • 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
      • 客户端发出段10,应答服务器的关闭连接请求。

Socket编程

预备知识

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
    • 比如发送数据1000(0x3e8):先发0x03,再发0xe8
    • 如果发送主机是小端字节序, 把1000填到发送缓冲区之前需要做字节序的转换
    • 如果接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换
  • 相关字节序转换的函数:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);// 将32位的长整数从主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • sockaddr数据结构(结构体):
  • sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换
// 字符串转in_addr的函数
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
// in_addr转字符串的函数:
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 例子:
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char * argv[]) {
    struct in_addr addr;
    if ( argc != 2 ) { 
        fprintf(stderr, "%s <dotted-address>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if (inet_aton(argv[1],&addr) == 0) {
        perror("inet_aton");
        exit(EXIT_FAILURE);
    }   
    printf("%s\n",inet_ntoa(addr));
    exit(EXIT_SUCCESS);
}

基于TCP协议的网络程序

  • TCP协议通讯流程
  • socket简单通信例子:
// server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000

int main () {
    struct sockaddr_in servaddr,cliaddr;// socket ipv4 地址结构体
    socklen_t cliaddr_len; // 客户端地址结构体长度
    char buf[MAXLINE];// 数据
    char str[INET_ADDRSTRLEN];// ip地址字符串
    int listenfd,connfd;// 文件描述符
    int i,n;
    listenfd = socket(AF_INET,SOCK_STREAM,0);// socket()打开一个网络通讯端口
    
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;// ipv4网络协议
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// ip地址字节序转换,INADDR_ANY表示0.0.0.0
    servaddr.sin_port = htonl(SERV_PORT);// 端口字节序转换
    
    bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));// 将参数sockfd和myaddr绑定在一起
    
    listen(listenfd,20);// listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态
    
    printf("Accepting connections ...\n");
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);// cliaddr传出参数, cliaddr_len传入传出参数
        n = read(connfd,buf,MAXLINE);
        printf("receivd from %s at PORT %d\n",
                        inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
        for (i = 0; i < n; i++) {
            buf[i] = toupper(buf[i]);
        }   
        write(connfd,buf,n);
        close(connfd);
    }   
}
// client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;
    char *str;
    
    if (argc != 2) {
        fputs("usage: ./client message\n", stderr);
        exit(1);
    }
    str = argv[1];
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    write(sockfd, str, strlen(str));

    n = read(sockfd, buf, MAXLINE);// 调用read从网络读就会阻塞
    printf("Response from server:\n");
    write(STDOUT_FILENO, buf, n);// 写常规文件是不会阻塞的,而向终端设备或网络写则不一定

    close(sockfd);
    return 0;
}

select

  • select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server