分类 linux基础 下的文章

Linux常见信号大全

表格

| 编号 | 信号名称 | 缺省动作 | 说明 |
| ---- | ----------------- | -------- | ------------------------------ |
| 1 | SIGHUP | 终止 | 终止控制终端或进程 |
| 2 | SIGINT | 终止 | 键盘产生的中断(Ctrl-C) |
| 3 | SIGQUIT | dump | 键盘产生的退出 |
| 4 | SIGILL | dump | 非法指令 |
| 5 | SIGTRAP | dump | debug中断 |
| 6 | SIGABRT/SIGIOT | dump | 异常中止 |
| 7 | SIGBUS/SIGEMT | dump | 总线异常/EMT指令 |
| 8 | SIGFPE | dump | 浮点运算溢出 |
| 9 | SIGKILL | 终止 | 强制进程终止 |
| 10 | SIGUSR1 | 终止 | 用户信号,进程可自定义用途 |
| 11 | SIGSEGV | dump | 非法内存地址引用 |
| 12 | SIGUSR2 | 终止 | 用户信号,进程可自定义用途 |
| 13 | SIGPIPE | 终止 | 向某个没有读取的管道中写入数据 |
| 14 | SIGALRM | 终止 | 时钟中断(闹钟) |
| 15 | SIGTERM | 终止 | 进程终止 |
| 16 | SIGSTKFLT | 终止 | 协处理器栈错误 |
| 17 | SIGCHLD | 忽略 | 子进程退出或中断 |
| 18 | SIGCONT | 继续 | 如进程停止状态则开始运行 |
| 19 | SIGSTOP | 停止 | 停止进程运行 |
| 20 | SIGSTP | 停止 | 键盘产生的停止 |
| 21 | SIGTTIN | 停止 | 后台进程请求输入 |
| 22 | SIGTTOU | 停止 | 后台进程请求输出 |
| 23 | SIGURG | 忽略 | socket发生紧急情况 |
| 24 | SIGXCPU | dump | CPU时间限制被打破 |
| 25 | SIGXFSZ | dump | 文件大小限制被打破 |
| 26 | SIGVTALRM | 终止 | 虚拟定时时钟 |
| 27 | SIGPROF | 终止 | profile timer clock |
| 28 | SIGWINCH | 忽略 | 窗口尺寸调整 |
| 29 | SIGIO/SIGPOLL | 终止 | I/O可用 |
| 30 | SIGPWR | 终止 | 电源异常 |
| 31 | SIGSYS/SYSUNUSED | dump | 系统调用异常 |

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

linux下硬盘的分割,格式化,检验和挂载(鸟哥的linux私房菜)

MBR分割和限制

  • 开机管理程式记录区与分割表通通放在磁碟的第一个磁区
    • 主要开机记录区(446字节)
    • 分割表(64字节), 所谓"分割"就是针对这64个字节
    • 分割表只有64字节,所以最多有四组记录区
  • 磁碟分割示意图:
  • 延展分割(利用额外的磁区记录分割信息):
  • 主要分割和延展分割特性:
    • 主要分割和延展分割最多四笔
    • 延展分割最多一个
    • 逻辑分割是又延展分割持续分割出来的分割槽
    • 逻辑分割可以格式化, 延展分割无法格式化

给系统新增一个硬盘

  • 对硬盘进行分割, 建立可用的partion;
  • 对partition进行格式化, 建立系统可用的filesystem;
  • 对刚刚建立好的filesystem进行检验
  • 在linux系统上, 建立挂载点(目录), 并挂载硬盘
  • 硬盘分割主要有MBR和GPT两种格式

lsblk列出系统上所有硬盘列表

NAME   MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda    253:0    0  20G  0 disk 
└─vda1 253:1    0  20G  0 part /
vdb    253:16   0   2G  0 disk [SWAP]
# 输出解析
NAME:装置名
MAJ:MIN:主要:次要装置代码
RM:是否为可卸载装置(如USB)
SIZE:容量
RO:是否为只读装置
TYPE:硬盘(disk),分割槽(partition),内存(rom)
MOUNTPOINT:挂载点
  • UUID:linux会为系统内所有装置都给以一个独一无二的标识码,可以用作挂载或者使用这个装置/档案系统之用

parted列出硬盘的分割表类型和分割信息

[root@xiaobai ~]# parted /dev/vda print
Model: Virtio Block Device (virtblk) # 硬盘的模组名称(厂商)
Disk /dev/vda: 21.5GB # 硬盘总容量
Sector size (logical/physical): 512B/512B # 硬盘的每个逻辑/物理磁区的容量
Partition Table: msdos # 分割方式 (MBR/GPT)
Disk Flags: 

Number  Start   End     Size    Type     File system  标志
 1      1049kB  21.5GB  21.5GB  primary  ext3         启动

硬盘分割:gdisk/fdisk

  • MBR分割使用fdisk命令, GPT分割使用gdisk命令
[root@xiaobai ~]# fdisk /dev/vda
欢迎使用 fdisk (util-linux 2.23.2)。

更改将停留在内存中,直到您决定将更改写入磁盘。
使用写入命令前请三思。


命令(输入 m 获取帮助):m
命令操作
   a   toggle a bootable flag
   b   edit bsd disklabel
   c   toggle the dos compatibility flag
   d   delete a partition # 删除一个分区
   g   create a new empty GPT partition table
   G   create an IRIX (SGI) partition table
   l   list known partition types
   m   print this menu
   n   add a new partition # 增加一个分区
   o   create a new empty DOS partition table
   p   print the partition table # 打印出分区列表
   q   quit without saving changes # 不保存离开
   s   create a new empty Sun disklabel
   t   change a partition's system id
   u   change display/entry units
   v   verify the partition table
   w   write table to disk and exit # 保存离开
   x   extra functionality (experts only)
命令(输入 m 获取帮助):p

磁盘 /dev/vda:21.5 GB, 21474836480 字节,41943040 个扇区
Units = 扇区 of 1 * 512 = 512 bytes
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
磁盘标签类型:dos
磁盘标识符:0x000c6178

   设备 Boot      Start(开始磁区标号)   End(结束磁区标号)   Blocks   Id  System
/dev/vda1   *     2048                      41943039    20970496  83  Linux

partprobe 更新linux核心的分割表信息

  • 分割保存后, 分割表没有更新, 可以通过两种方式更新1. 重启;2. partprobe命令

硬盘格式化

  • mkfs.xfs
  • mkfs.ext4

档案系统检验

  • xfs_repair
    • 修复时该文件系统不能被挂载, 但根目录无法被卸载, 需要进入'单人维护'/'救援模式'
  • fsck.ext4

档案系统的挂载和卸载 mount/unmount

mount -a
mount [-l]
mount [-t 档案系統] LABEL=''  挂载点
mount [-t 档案系統] UUID=''   挂载点
mount [-t 档案系統] 装置名称   挂载点

umount [-fn] 装置名称或挂载点
参数數:
-f  :強制卸载
-l  :立刻卸载
-n  :不更新 /etc/mtab 情況下卸载

开机自动挂载配置文件/etc/fstab

[root@xiaobai bin]# cat /etc/fstab
[装置/UUID等]        [挂载点]             [文件系統]  [文件系統參數]          [dump]  [fsck]
/dev/vda1            /                    ext3       noatime,acl,user_xattr 1 1
LABEL=lswap          swap                 swap       defaults 0 0
proc                 /proc                proc       defaults              0 0
sysfs                /sys                 sysfs      noauto                0 0
debugfs              /sys/kernel/debug    debugfs    noauto                0 0
devpts               /dev/pts             devpts     mode=0620,gid=5       0 0

linux下man命令

刚接触linux的时候, 经常需要使用man命令查看其他命令的使用方式, 其实man还包括其他很多帮助文档

man 命令是按照章节存储的,Linux的man手册共有以下几个章节:

  1. General Commands 用户在shell中可以操作的指令或者可执行文档
  2. System Calls 系统调用的函数与工具等
  3. Sunroutines C语言库函数
  4. Special Files 设备或者特殊文件
  5. File Formats 文件格式与规则
  6. Games 游戏及其他
  7. Macros and Conventions 表示宏、包及其他杂项
  8. Maintenence Commands 表示系统管理员相关的命令
  9. 其他
  10. 在线man手册地址