【Linux】多进程编程

目录

1. 进程基础知识

2. 查看进程

2.1 ps命令

2.2 /proc系统文件夹

2.3 top命令(实时显示进程动态)

3. 杀死进程

4. 获取进程标识符

5. 进程地址空间

6. 进程创建

7. 进程终止

8. 进程等待

9. 进程程序替换

10. 进程间通信之管道

10.1 匿名管道

10.2 命名管道(FIFO)

11. 进程间通信之共享内存

12. 进程间通信之信号

12.1 Linux信号

12.2 产生信号

12.3 捕捉信号

12.4 信号集

12.4.1 未决信号集和阻塞信号集

12.4.2 信号集函数


1. 进程基础知识

详见

5.1 进程、线程基础知识 | 小林codingicon-default.png?t=N7T8https://xiaolincoding.com/os/4_process/process_base.html#%E8%BF%9B%E7%A8%8B

2. 查看进程

2.1 ps命令

ps aux / ajx
# a    显示终端上的所有进程,包括其他用户的进程
# u    面向用户格式
# j    作业控制格式
# x    显示没有控制终端的进程

第一行表示进程的属性,就像表头一样,其中STAT表示进程状态:

运行一个死循环的程序,并查看该进程:

1. 运行程序

2. 为了方便操作和观察,复制SSH渠道并分屏:

3. 查看进程

2.2 /proc系统文件夹

ls /proc/pid    # 查看PID为pid的进程信息

2.3 top命令(实时显示进程动态)

top         # 默认3s刷新一次
top -d 5    # 5s刷新一次
top -d 8    # 8s刷新一次

在top命令执行后,可以按以下按键对显示的结果进行排序:

  • M    根据内存使用量排序
  • P    根据CPU占有率排序
  • T    根据进程运行时间长短排序
  • U    根据用户名来筛选进程
  • K    输入指定的PID杀死进程

3. 杀死进程

kill PID       # 默认信号是15,等价于kill -15 PID,或kill -SIGTERM PID
kill -9 PID    # 强制杀死,等价于kill -SIGKILL PID
killall name   # 根据进程名杀死进程
kill -l        # 列出所有信号

4. 获取进程标识符

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);  // 获取进程ID
pid_t getppid(void); // 获取父进程ID

5. 进程地址空间

每个进程都有一套独立的虚拟地址空间,进程持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。

虚拟地址与物理地址之间通过页表来映射,如下图:

6. 进程创建

#include <unistd.h>
pid_t fork(void);
// 父进程返回子进程ID,子进程返回0;失败时返回-1并设置errno

读时共享,写时拷贝:

子进程的代码与父进程完全相同,同时它还会拷贝父进程的数据。数据的拷贝采用写时拷贝(copy on writte)技术,即只有在任一进程(父进程或子进程)对数据执行了写操作时,拷贝才会发生。

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h> // pid_t在sys/types.h头文件中

int gval = 10;

int main()
{
    int lval = 20;
    gval++;
    lval += 5;

    pid_t pid = fork();
    if (pid == 0)     // 子进程
    {
        gval += 2;
        lval += 2;
    }
    else if (pid > 0) // 父进程
    {
        gval -= 2;
        lval -= 2;
    }

    if (pid == 0)     // 子进程
    {
        printf("子进程 gval: %2d, lval: %2d\n", gval, lval);
    }
    else if (pid > 0) // 父进程
    {
        printf("父进程 gval: %2d, lval: %2d\n", gval, lval);
    }

    return 0;
}
// 父进程 gval:  9, lval: 23
// 子进程 gval: 13, lval: 27

7. 进程终止

正常终止(可以通过echo $?查看进程退出码):

  • C标准库函数exit
  • 系统调用接口_exit
  • main函数内部return语句(main函数内部,return n等价于exit(n))

异常终止:

  • Ctrl+C,信号终止
#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);

// status    进程退出码

C标准库函数exit和系统调用接口_exit的区别:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello");
    exit(0);
    return 0;
}

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("hello");
    _exit(0);
    return 0;
}

8. 进程等待

父进程运行结束,但子进程还在运行,这样的子进程就称为孤儿进程。孤儿进程将被init进程(PID为1)所收养,并由init进程对它们完成状态收集工作。因此,孤儿进程并不会有什么危害。

子进程运行结束,而父进程又没有回收子进程、释放子进程占用的资源,此时子进程将成为一个僵尸进程。僵尸进程不能被kill -9杀死。

僵尸进程的危害:

  • 占用系统资源,造成资源浪费。
  • 占用PID,如果存在大量的僵尸进程,将因为没有可用的PID而导致系统不能产生新的进程。

父进程应该等待子进程运行结束,回收子进程资源,获取子进程退出状态信息,才能避免僵尸进程。

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);
// wait函数将阻塞进程,直到该进程的某个子进程运行结束
// 成功时返回运行结束的子进程ID,失败时返回-1
// status    输出型参数,获取子进程退出状态信息,不关心则设置为NULL

pid_t waitpid(pid_t pid, int* status, int options);
// waitpid只等待由pid参数指定的子进程
// 成功时返回运行结束的子进程ID,失败时返回-1
// pid        目标子进程ID,如果值为-1,则和wait函数相同,即等待任意一个子进程结束
// status     和wait函数相同
// options    可以控制waitpid函数的行为,通常为WNOHANG,此时waitpid调用是非阻塞的
//            如果目标子进程还没有结束或意外终止,则waitpid立即返回0

status是位图:

退出码和信号不同时存在。

位运算获取退出码:(status >> 8) & 0xFF    (二进制11111111=十六进制FF)

位运算获取信号:     status & 0x7F             (二进制01111111=十六进制7F)

也可通过退出状态信息相关的宏函数获取退出码和信号:

WIFEXITED(status)    // 非0,进程正常终止
WEXITSTATUS(status)  // 如果上宏为真,获取进程退出码

WIFSIGNALED(status)  // 非0,进程异常终止
WTERMSIG(status)     // 如果上宏为真,获取使进程终止的信号编号

WIFSTOPPED(status)   // 非0,进程处于暂停状态
WSTOPSIG(status)     // 如果上宏为真,获取使进程暂停的信号编号

WIFCONTINUED(status) // 非0,进程暂停后已经继续运行
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int status;

    pid_t pid = fork(); // 创建子进程1
    if (pid == 0)       // 子进程1
    {
        return 3;       // 子进程1终止
    }
    else if (pid > 0)   // 父进程
    {
        printf("子进程1的PID: %d\n", pid);
        pid = fork();     // 创建子进程2
        if (pid == 0)     // 子进程2
        {
            exit(7);      // 子进程2终止
        }
        else if (pid > 0) // 父进程
        {
            printf("子进程2的PID: %d\n", pid);
            wait(&status);         // 回收子进程资源,获取子进程退出状态信息
            if (WIFEXITED(status)) // 判断子进程是否正常终止
            {
                printf("子进程退出码: %d\n", WEXITSTATUS(status));
            }
            wait(&status);         // 因为之前创建了2个进程,所以再次调用wait函数和宏
            if (WIFEXITED(status))
            {
                printf("子进程退出码: %d\n", WEXITSTATUS(status));
            }
            sleep(30); // 为暂停父进程终止而插入的代码,此时可以查看子进程的状态
        }
    }
    return 0;
}
// 子进程1的PID: 11086
// 子进程2的PID: 11087
// 子进程退出码: 3
// 子进程退出码: 7
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int status;

    pid_t pid = fork(); // 创建子进程
    if (pid == 0)       // 子进程
    {
        sleep(15);      // 子进程延迟15s
        return 24;
    }
    else if (pid > 0)   // 父进程
    {
        while (!waitpid(-1, &status, WNOHANG)) // 这个waitpid调用是非阻塞的
        {
            sleep(3);
            puts("sleep 3sec.");
        }
        if (WIFEXITED(status))
        {
            printf("子进程退出状态码: %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// 子进程退出状态码: 24

9. 进程程序替换

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的ID并未改变。

exec函数族:

#include <unistd.h>
// 系统调用接口
int execve(const char* filename, char* const argv[], char* const envp[]);
// 标准C库函数(底层都调用execve)
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
// 成功时不返回,原程序中exec调用之后的代码都不会执行(被替换了);失败时返回-1并设置errno

// 参数:
// path    可执行文件路径,绝对路径、相对路径均可
// file    可执行文件名称,该文件的具体位置在环境变量PATH中搜索
// arg     可执行文件的命令行参数列表
// argv    可执行文件的命令行参数数组
// arg和argv都会被传递给可执行文件的main函数
// envp    新进程的环境变量

// 命名:
// l(list)           命令行参数列表
// v(vector)         命令行参数数组
// p(path)           在环境变量PATH中搜索file
// e(environment)    自定义环境变量

使用方法示例(以在进程内部执行ls命令为例):

// 命令行参数数组
char* argv[] = {"ls", "-a", "-l", NULL};
// 自定义环境变量,是覆盖式传入,想要添加或修改环境变量用putenv函数
char* envp[] = {"PATH=/usr/local/bin:/usr/bin", NULL};

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execv("/usr/bin/ls", argv);

execlp("ls", "ls", "-a", "-l", NULL);
execvp("ls", argv);

execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);

execvpe("ls", argv, envp);

10. 进程间通信之管道

进程间通信(Inter Process Communication),简称IPC。

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的。

管道和信号依赖于内核,但共享内存无需内核介入。

10.1 匿名管道

管道符 |:将管道符左边命令的输出结果,作为右边命令的输入。

这种管道是没有名字的,称为匿名管道,用完了就销毁。

匿名管道的创建,需要通过下面这个系统调用:

#include <unistd.h>
int pipe(int pipefd[2]);
// 成功时返回0,失败时返回-1并设置errno
// pipefd    输出型参数,pipefd[0]保存管道读端文件描述符,pipefd[1]保存管道写端文件描述符
// 记忆:0像一张嘴,是读端,1像一支笔,是写段

管道的本质是内核中的一个缓冲区,缓冲区的存储能力是有限的。

管道默认是阻塞的:如果管道中没有数据,read阻塞;如果管道满了,write阻塞。

如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),有进程向管道的写端写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。

如何用管道实现进程间通信?

我们可以使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个fd[0]与fd[1],两个进程就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。

管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。如果仅需要父进程给子进程通信,通常的做法是:

  • 父进程关闭读取的fd[0],只保留写入的fd[1];
  • 子进程关闭写入的fd[1],只保留读取的fd[0];

所以说如果需要双向通信,则应该创建两个管道。

到这里,我们仅仅解析了使用管道进行父进程与子进程之间的通信,但是在我们shell里面并不是这样的。

在shell里面执行A | B命令的时候,A进程和B进程都是shell创建出来的子进程,A和B之间不存在父子关系,它俩的父进程都是shell。

所以说,在shell里通过“|”匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写shell脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。

子进程发送数据给父进程,父进程读取到数据,然后打印:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
        perror("pipe");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == 0) // 子进程
    {
        printf("我是子进程,我的PID为%d\n", getpid());

        close(pipefd[0]); // 关闭子进程的读端

        // 子进程每隔2s不断写入数据
        while (1)
        {
            char* msg = "这是子进程给父进程发送的数据";
            write(pipefd[1], msg, strlen(msg));
            sleep(1);
        }
    }
    else if (pid > 0) // 父进程
    {
        printf("我是父进程,我的PID为%d\n", getpid());
        close(pipefd[1]); // 关闭父进程的写端
        
        // 父进程不断读取数据到buf数组中,并打印
        while (1)
        {
            char buf[100] = "";
            int ret = read(pipefd[0], buf, sizeof(buf));
            if (ret == -1)
            {
                perror("read");
            }
            else if (ret == 0)
            {
                break;
            }
            else
            {
                printf("父进程读取到的数据:%s\n", buf);        
            }
        }
    }
    return 0;
}

10.2 命名管道(FIFO)

匿名管道只能用于具有亲缘关系(父子进程、兄弟进程)的进程间通信。因为匿名管道没有名字,即没有管道文件,只能通过fork来拷贝父进程打开的文件描述符,实现利用匿名管道的进程间通信。

为了解决匿名管道的这种问题,提出了命名管道(FIFO)。命名管道在不相关的进程间也能相互通信,因为命名管道以FIFO的文件形式存在于文件系统中,其打开方式和普通文件是一样的,只要可以使用管道文件,就可以相互通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出(FIFO)原则,不支持lseek之类的文件定位操作。这也是命名管道的另一个名称——FIFO的由来。

创建命名管道:

  • mkfifo命令
mkfifo 管道的文件名
  • mkfifo函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
// 成功时返回0,失败时返回-1并设置errno
// pathname    FIFO的文件名
// mode        文件权限

实现简单聊天功能:

A.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 判断有名管道fifo1是否存在,不存在则创建
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 判断有名管道fifo2是否存在,不存在则创建
    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 以只写的方式打开管道fifo1
    int fdw = open("fifo1", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(-1);
    }
    printf("打开管道fifo1成功,等待写入...\n");

    // 以只读的方式打开管道fifo2
    int fdr = open("fifo2", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(-1);
    }
    printf("打开管道fifo2成功,等待读取...\n");

    char buf[128];

    // 循环地写读数据
    while (1)
    {
        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);

        // 向管道中写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(-1);
        }

        // 从管道中读数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }
        printf("buf: %s\n", buf);
    }

    // 关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

B.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 判断有名管道fifo1是否存在,不存在则创建
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 判断有名管道fifo2是否存在,不存在则创建
    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 以只读的方式打开管道fifo1
    int fdr = open("fifo1", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(-1);
    }
    printf("打开管道fifo1成功,等待读取...\n");

    // 以只写的方式打开管道fifo2
    int fdw = open("fifo2", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(-1);
    }
    printf("打开管道fifo2成功,等待写入...\n");

    char buf[128];

    // 循环地读写数据
    while (1)
    {
        // 从管道中读数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }
        printf("buf: %s\n", buf);

        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);

        // 向管道中写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(-1);
        }
    }

    // 关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

Makefile:

.PHONY:all
all: A B

A:A.c
	gcc $^ -o $@
B:B.c
	gcc $^ -o $@

.PHONY:clean
clean:
	rm -f A B

11. 进程间通信之共享内存

共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。

管理共享内存信息的结构体:

struct shmid_ds
{
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
};

Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmget、shmat、shmdt和shmctl。

shmget

shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。其定义如下:

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 成功时返回共享内存的标识符,失败时返回-1并设置errno
// key       标识共享内存段,key_t是一个整型
// size      共享内存的大小
//           如果是创建新的共享内存,则size值必须被指定;如果是获取已经存在的共享内存,则可以把size设置为0
// shmflg    权限(如0666)和附加属性
//           附加属性:IPC_CREAT:创建共享内存
//                    IPC_EXCL:判断共享内存是否存在,需要和IPC_CREAT一起使用
//           示例:IPC_CREAT | IPC_EXCL | 0666

如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化。

shmat和shmdt

共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。这两项任务分别由如下两个系统调用实现:

#include <sys/shm.h>
void* shmat(int shmid, const void* shmaddr, int shmflg);
// 成功时返回共享内存的首地址,失败时返回(void*)-1并设置errno
// shmid      共享内存的标识符,即shmget的返回值
// shmaddr    指定将共享内存关联到进程的哪块地址空间,通常为NULL,让内核指定
// shmflg     对共享内存的操作:SHM_RDONLY:只读模式
//                              0:读写模式
int shmdt(const void* shmaddr);
// 成功时返回0,失败时返回-1并设置errno
// shmaddr    共享内存的首地址

shmctl

shmctl系统调用控制共享内存的某些属性。其定义如下:

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 成功时返回0,失败时返回-1并设置errno
// shmid      共享内存的标识符,即shmget的返回值
// cmd        对共享内存的操作
//            IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中
//            IPC_SET :设置共享内存的状态,把buf的uid、gid、mode复制到共享内存的shmid_ds结构中
//            IPC_RMID:删除共享内存段
// buf        指向管理共享内存信息的结构体

12. 进程间通信之信号

12.1 Linux信号

前31个信号是常规信号,其余为实时信号。关于信号的详细信息可以用man 7 signal查看。

信号的默认动作:

Linux信号表

   编号

(宏值)

  信号

(宏名)

默认动作含义
1SIGHUPTerm控制终端挂起
2SIGINTTerm键盘输入以中断进程(Ctrl+C)
3SIGQUITCore键盘输入使进程退出(Ctrl+\)
4SIGILLCore非法指令
5SIGTRAPCore断点陷阱,用于调试
6SIGABRTCore进程调用abort函数时生成该信号
7SIGBUSCore总线错误,错误内存访间
8SIGFPECore浮点异常
9

SIGKILL

Term终止一个进程。该信号不可被捕获或者忽略
10SIGUSR1Term用户自定义信号之一
11SIGSEGVCore非法内存段引用
12SIGUSR2Term用户自定义信号之二
13SIGPIPETerm往读端被关闭的管道或者socket连接中写数据
14SIGALRMTerm由alarm或setitimer设置的实时闹钟超时引起
15SIGTERMTerm终止进程。kill命令默认发送的信号就是SIGTERM
16SIGSTKFLTTerm早期的Linux使用该信号来报告数学协处理器栈错误
17SIGCHLDIgn子进程状态发生变化(退出或者暂停)
18SIGCONTCont启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略
19SlGSTOPStop暂停进程(Ctrl+S)。该信号不可被捕获或者忽略
20SIGTSTPStop挂起进程(Ctrl+Z)
21SIGTTINStop后台进程试图从终端读取输入
22SIGTTOUStop后台进程试图往终端输出内容
23SIGURGIgnsocket连接上接收到紧急数据
24SIGXCPUCore进程的CPU使用时间超过其软限制
25SIGXFSZCore文件尺寸超过其软限制
26

SIGVTALRM

Term与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间
27SIGPROFTerm与SIGALRM类似,它同时统计用户代码和内核的运行时间
28SIGWINCHIgn终端窗口大小发生变化
29SIGIOTermIO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中使用。SIGIO信号可用在UDP服务器中,不过也非常少见
30SIGPWRTerm对于使用UPS(Uninterruptable Power Supply)的系统,当电池电量过低时,SIGPWR信号将被触发
31SIGSYSCore非法系统调用

12.2 产生信号

  • 终端按键,如Ctrl+C产生SIGINT(2)信号,Ctrl+Z产生SIGTSTP(20)信号。
  • 硬件异常,如除0操作产生SIGFPE(8)信号,非法访问内存产生SIGSEGV(11)信号。
  • 软件异常,如往读端被关闭的管道或者socket连接中写数据产生SIGPIPE(13)信号,alarm或setitimer设置的定时器时间到产生SIGALRM(14)信号。
  • 系统调用,如kill函数给任何进程或进程组发送信号,raise函数给当前进程发送信号,abort函数给当前进程发送SIGABRT(6)信号。
  • kill命令,格式:kill [-信号的宏值或宏名] PID,本质是调用kill函数实现的。

kill、raise、abort

#include <signal.h>
int kill(pid_t pid, int sig);
// 给任何进程或进程组发送信号
// 成功时返回0,失败时返回-1并设置errno
// pid>0     信号发送给PID为pid的进程
// pid=0     信号发送给本进程组内的其他进程
// pid=-1    信号发送给除init进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限
// pid<-1    信号发送给组ID为-pid的进程组中的所有成员
// sig       要发送的信号的宏值或宏名,0表示不发送任何信号

#include <signal.h>
int raise(int sig);
// 给当前进程发送信号
// 成功时返回0,失败时返回非0

#include <stdlib.h>
void abort(void);
// 给当前进程发送SIGABRT信号,异常终止该进程

alarm、setitimer

#include <unistd.h>
unsigned alarm(unsigned seconds);
// 设置定时器(闹钟),seconds秒之后向当前进程发送SIGALAR信号,该信号的默认动作是结束进程
// 之前没有定时器,返回0;之前有定时器,返回之前的定时器剩余的时间

#include <sys/time.h>
int setitimer(int which, const struct itimerval* restrict value, 
              struct itimerval* restrict ovalue);
// 设置周期性定时器(闹钟),将which指定的定时器设置为value指向的结构体中指定的值,精度微秒
// 成功时返回0,失败时返回-1并设置errno
// which     定时器类型
//           ITIMER_REAL   :以真实时间来计算,时间到发送SIGALRM
//           ITIMER_VIRTUAL:以该进程在用户态下所消耗的时间来计算,时间到发送SIGVTALRM
//           ITIMER_PROF   :以该进程在用户态和内核态下所消耗的时间来计算,时间到发送SIGPROF
// value     新的定时器
// ovalue    旧的定时器,一般不使用,通常为NULL

struct itimerval // 管理定时器的结构体
{
    struct timeval it_interval; // 时间间隔
    struct timeval it_value;    // 计时时长
};
// 第一次计时it_value时长发送信号,再往后的信号每隔一个it_interval发送一次
struct timeval // 管理时间的结构体
{        
    time_t      tv_sec;     //  秒数     
    suseconds_t tv_usec;    //  微秒    
};

12.3 捕捉信号

signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// man 2 signal查询,手册说应该避免使用它,请使用sigaction
// 成功时返回前一次调用signal函数时传入的函数指针
// 如果是第一次调用signal,返回信号signum对应的默认处理函数指针SIG_DEF
// 失败时返回SIG_ERR并设置errno
// signum     要捕捉的信号的宏值或宏名,除SIGKILL(9)和SIGSTOP(19)
// handler    捕捉到的信号如何处理
//            SIG_IGN:忽略信号
//            SIG_DFL:使用信号默认动作
//            自定义信号处理函数,是回调函数

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
// 成功时返回0,失败时返回-1并设置errno
// signum    要捕捉的信号的宏值或宏名,除SIGKILL(9)和SIGSTOP(19)
// act       捕捉到的信号如何处理
// oldact    上一次的处理动作,一般不使用,通常为NULL

struct sigaction
{
    void     (*sa_handler)(int); // SIG_IGN或SIG_DFL或自定义信号处理函数(回调函数)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 不常用
    sigset_t   sa_mask;  // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
    int        sa_flags; // 通常为0,表示使用默认属性(即使用sa_handler)
    void     (*sa_restorer)(void); // 已弃用
};

12.4 信号集

12.4.1 未决信号集和阻塞信号集

信号递达、未决、阻塞的概念:

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

图片来源:程序员成长之旅——进程信号_sigaction-CSDN博客

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

/* A `sigset_t' has a bit for each signal.  */

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

12.4.2 信号集函数

#include <signal.h>

int sigemptyset(sigset_t* set);
// 清空信号集(将信号集中所有标志位置为0)
// 成功时返回0, 失败时返回-1

int sigfillset(sigset_t* set);
// 设置所有信号(将信号集中所有标志位置为1)
// 成功时返回0, 失败时返回-1

int sigaddset(sigset_t* set, int signum);
// 将信号signum添加到信号集中(对应的标志位置为1)
// 成功时返回0, 失败时返回-1

int sigdelset(sigset_t* set, int signum);
// 将信号signum从信号集中删除(对应的标志位置为0)
// 成功时返回0, 失败时返回-1

int sigismember(const sigset_t* set, int signum);
// 判断signum是否在信号集中
// 在返回1,不在返回0,失败时返回-1
#include <signal.h>

int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
// 将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
// 成功时返回0, 失败时返回-1并设置errno
// how       如何对内核阻塞信号集进行处理
//           SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
//                   假设内核中默认的阻塞信号集是mask, mask | set
//           SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
//                   mask &= ~set
//           SIG_SETMASK:覆盖内核中原来的值            
// set       已经初始化好的用户自定义的信号集
// oldset    保存设置之前的内核中的阻塞信号集的状态,可以是NULL

int sigpending(sigset_t* set);
// 获取内核中的未决信号集
// 成功时返回0, 失败时返回-1并设置errno
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值