首页 / 资讯中心 / 文章详情

Linux 进程、线程与进程通信核心知识笔记

Linux 进程、线程与进程通信核心知识笔记 ★ FEATURED ARTICLE
一、进程进程由 PCB 与进程地址空间组成地址空间自上而下依次为堆、栈、bss未初始化全局变量、data已初始化全局变量、text代码段。堆存放通过 malloc、new 等手动申请的动态内存其生命周期由程序员自行控制。栈存放函数调用过程中自动管理的数据包括命令行参数、环境变量、函数参数、返回地址、寄存器现场以及非静态局部变量等随函数进入和退出自动分配与释放。进程号pid最小为 1。常用进程查看与操作命令如下top以任务管理器方式实时查看进程。ps aux | grep a.out查看 a.out 进程的快照包含 pid 号与 stat 状态信息。ps -elf | grep a.out查看子进程及其父进程ppid。kill -l查询各信号序号及其含义。kill -序号 进程pid号向指定 pid 的进程发送对应信号。killall -序号 执行文件名无需 pid 号按可执行文件名批量发送信号。pstree以树形结构显示全部进程。pstree -sp 进程pid号查看指定进程的关系链。进程状态R运行或就绪正在运行或等待 CPU。S可中断睡眠等待事件可被信号唤醒。D不可中断睡眠等待 I/O无法被信号杀死。T暂停或跟踪被挂起或处于调试中。Z僵尸已结束但尚未被父进程回收。进程创建下面给出一个完整的 fork() 使用示例通过 if 分支区分父进程与子进程的执行逻辑#include stdio.h #include unistd.h int main(void) { pid_t pid fork(); // 创建子进程 if (pid 0) { // 父进程分支fork 返回子进程的 pid 号 printf(父进程我是父进程我的 pid %d子进程的 pid %d\n, getpid(), pid); } else if (pid 0) { // 子进程分支fork 返回 0 printf(子进程我是子进程我的 pid %d父进程的 pid %d\n, getpid(), getppid()); } else { // 失败分支返回 -1 perror(fork); return -1; } return 0; }执行结果示例父子进程执行顺序不确定pid 号每次运行可能不同父进程我是父进程我的 pid 1234子进程的 pid 1235 子进程我是子进程我的 pid 1235父进程的 pid 1234说明fork() 成功时父进程返回子进程的 pid 号子进程返回 0失败时返回 -1。通过 if 分支可以让父子进程各自执行不同的代码段。函数原型pid_t 进程名 fork(); // 库unistd.h参数无返回值成功时父进程返回子进程 pid 号子进程返回 0失败时返回 -1并设置 errno。fork() 的本质是创建新进程子进程等价于复制调用进程父进程生成新进程。由于该函数的功能是复制父子进程的代码段相同但子进程不会再次执行从父进程复制来的创建进程代码段而是直接返回 0。父子进程的 PCB 相互独立而堆、栈、bss、data、text 的内容相同。父子进程事件不独立直接输入即可无需通过判断区分。父子进程事件独立父进程中 fork 返回子进程的 pid 号子进程中 fork 不会再次执行而是返回 0因此可通过分支区分if (进程名 0) { // 父进程执行内容 } else if (进程名 0) { // 子进程执行内容 }常用配套函数getpid()获得当前进程的 pid 号。getppid()获得当前进程父进程的 pid 号。printf(%d, pid)显示该 pid 号进程的相关信息。使用 fork() 时需注意以下几点父子进程的执行顺序不确定。父子进程各自拥有独立的地址空间堆、栈、bss、data、text相互独立。每个进程各自拥有 4GB 虚拟内存空间3GB 用户空间 1GB 内核空间。n 次 fork 会生成 2n-1 个新进程加上原本的进程共 2n 个。fork 后共同操作文件时子进程会拥有与父进程同名的文件描述符二者共享文件状态与文件偏移量可以操作同一个文件。为避免相互影响可通过上述 if 判断分别在各自进程中打开文件流进行操作。fork exec用于在子进程中执行完全不同的程序。exec 函数会用新的程序镜像替换当前进程的各个段其中 PCB 段保持不变。程序路径可通过终端命令which ls查询ls 为程序名称。exec 系列函数如下int execl(程序路径, 程序名称, l, ., NULL); int execv(程序路径, args); // args 内含程序路径以外的内容 // char * const args[] {ls, l, ., NULL};exec 会替换原进程的各个段即 PCB [堆 | 栈 | bss | data | text]。PATH系统环境变量记录当前系统各个二进制文件可执行文件的路径缩写为 p。可通过echo $PATH在终端查看。int execlp(程序名称, ls, l, ., NULL); int execvp(程序名称, args); // args 内含程序名称以外的内容 // char * const args[] {ls, l, ., NULL};environment环境变量缩写为 e。int execle(程序路径, env, NULL, environ);当程序运行时需要使用系统环境变量可使用该函数。使用 environ 前需先声明extern char **environ;也可自行定义环境变量例如char *ev[] {USER hello, PASSWD 12345, NULL}; execle(程序路径, 程序名称, NULL, ev);int execvpe(程序名称, args); // args 内含程序名称以及环境变量environ以外的内容编译时使用gcc 文件名.c -D_GNU_SOURCE不加该选项会告警但不影响运行。拆分字符串char *str strtok(字符串, ;;); // 库string.h返回值成功时返回拆分产生的子字符串首地址失败时返回 NULL。原理将输入字符串中的分隔字符全部替换为\0。若想继续拆分再次调用该函数时将第一个参数改为 NULL 即可。进程结束进程结束分为正常结束与异常结束两种正常结束从 main 函数返回或调用 exit / _exit 函数。异常结束调用 abort 终止或通过 signal 发送信号结束。正常结束进程可调用exit(传给父进程的状态信息); // 库stdlib.h返回值成功时返回输入的值 0377即只有最低八位有效。exit 在退出之前会刷新 I/O 缓存并调用已注册的退出处理函数。二、进程退出与资源回收注册退出清理函数atexit(退出清理的函数名); // 库stdlib.h返回值成功返回 0失败返回非零值。多个清理函数的调用顺序与注册顺序相反即先注册的后调用。退出清理函数示例void clean_up(void) { printf(clean_up\n); free(malloc函数申请到的空间名称); }_exit(传给父进程的状态信息); // 库stdlib.h与 exit 不同_exit 调用结束后不会处理 I/O 缓存也不会调用退出清理函数。进程资源回收wait(设定子进程退出的状态值); // 库sys/wait.h不在意可填 NULL返回值成功返回结束子进程的 PID 号失败返回 -1。wait 会等待子进程状态改变退出期间阻塞父进程执行且一次只能处理一个子进程。也可通过指针配合宏获取退出状态int *p; wait(p); // 通过宏 WIFEXITED(p) 判断是否正常结束 // 再通过宏 WEXITSTATUS(p) 获取子进程退出状态宏WIFEXITED()判断子进程是否正常结束。WEXITSTATUS()获取子进程退出状态。WIFSIGNALED()获取结束信号的编号。waitpid(pid, 保存子进程退出状态值, 0 或 WNOHANG); // 库sys/wait.h返回值阻塞调用0成功时返回成功回收的子进程 pid非阻塞调用WNOHANG时若子进程状态未改变则返回 0失败返回 -1。pid 参数的取值含义pid -1等待进程组 id 为 pid 绝对值的组中任意子进程状态改变常用。pid -1等待当前进程的任意子进程状态改变。pid 0等待与父进程 pid 相同进程组中的任意子进程状态改变常用。pid 0等待指定 pid 的进程状态改变。其中wait(p)等价于waitpid(-1, p, 0)均为阻塞调用。三、线程线程由线程 ID、程序计数器、寄存器集合和栈组成侧重执行同一进程内的线程共享数据与代码文件但寄存器与栈相互独立。线程的创建与切换效率高于进程线程依附于进程存在而进程在隔离性上更加安全稳定。errno为全局错误变量库为 errno.h。线程创建pthread_create(线程 ID, 线程属性, (void *)线程回调函数, 传递给线程执行函数的参数); // 线程属性填 NULL 表示默认属性资源回收方式库pthread.h。返回值成功返回 0失败返回错误码。编译时需要链接-lpthread。例如pthread_t tid; // 线程 id 号使用线程时需注意主线程与子线程关系对等。线程共享进程资源包括全局变量、局部变量和堆区变量。若需要多个线程共同影响同一份数据应传参地址并进行解引用*。若需要多线程共用一个初值且各自互不干扰则需通过其他办法获得初值而不改变它。线程结束pthread_exit(退出状态值的地址); // 库pthread.h退出当前线程pthread_join(线程 ID, NULL 或 指针); // 库pthread.h指针为 void* 类型用于保存状态值地址返回值成功返回 0失败返回错误码。pthread_join 会等待该 ID 的线程结束并获得其退出状态值返回的指针在使用时需要进行强制类型转换。如果主线程调用 pthread_exit会结束主线程的执行流但并不会结束整个进程进程会等待所有子线程结束后才销毁。取消线程pthread_cancel(线程 ID); // 库pthread.h返回值成功返回 0失败返回错误码用于取消对应线程。需要注意任意线程调用 exit 都会导致进程结束线程随之不复存在main 函数中的 return 同理。获取线程 IDpthread_self(); // 库pthread.h成功返回当前线程 ID更改线程状态pthread_detach(线程 ID); // 库pthread.h返回值成功返回 0失败返回错误码。该函数将指定 ID 的线程更改为可分离状态推荐在线程创建后立即调用。可结合线程调用 pthread_exit 退出调用 pthread_join 回收资源并获得退出状态值创建时默认可结合。可分离线程调用 pthread_detach 更改状态线程结束后自动回收。四、线程库——互斥锁使用临界资源时应遵循“先上锁、再使用、后解锁”的原则。当多个线程需要分别修改并共同影响同一份数据时就需要使用互斥锁库为 pthread.h。常用接口如下pthread_mutex_init创建互斥锁。pthread_mutex_lock线程上锁。pthread_mutex_unlock线程解锁。pthread_mutex_destroy销毁互斥锁。销毁互斥锁下面给出一个使用 pthread_mutex_t 保护临界资源的完整示例两个线程对共享变量 counter 进行递增操作#include stdio.h #include pthread.h #include unistd.h #define THREAD_NUM 2 #define LOOP_NUM 100000 int counter 0; // 共享变量临界资源 pthread_mutex_t mutex; // 定义互斥锁 // 线程回调函数对共享变量进行递增 void *thread_func(void *arg) { for (int i 0; i LOOP_NUM; i) { pthread_mutex_lock(mutex); // 上锁进入临界区前加锁 counter; // 临界区操作共享变量 pthread_mutex_unlock(mutex); // 解锁离开临界区后解锁 } return NULL; } int main(void) { pthread_t tid[THREAD_NUM]; // 创建互斥锁NULL 表示使用默认属性 pthread_mutex_init(mutex, NULL); // 创建两个线程 for (int i 0; i THREAD_NUM; i) { pthread_create(tid[i], NULL, thread_func, NULL); } // 等待两个线程结束 for (int i 0; i THREAD_NUM; i) { pthread_join(tid[i], NULL); } // 销毁互斥锁 pthread_mutex_destroy(mutex); printf(counter %d期望值 %d\n, counter, THREAD_NUM * LOOP_NUM); return 0; }编译运行gcc mutex_demo.c -o mutex_demo -lpthread ./mutex_demo运行结果示例counter 200000期望值 200000说明上锁每个线程在进入临界区前调用 pthread_mutex_lock保证同一时刻只有一个线程能操作 counter。解锁操作完成后调用 pthread_mutex_unlock释放锁让其他线程进入。销毁所有线程结束后调用 pthread_mutex_destroy 释放锁资源。如果不加锁两个线程同时递增 counter 会产生竞态条件最终结果会小于期望值 200000。各接口的详细用法如下// 定义锁 pthread_mutex_t 锁的名称; // 创建互斥锁锁的类型填 NULL 表示普通锁 pthread_mutex_init(锁的地址, 锁的类型); // 返回值成功 0失败 错误码 // 线程上锁 pthread_mutex_lock(应用的锁的地址); // 返回值成功 0失败 错误码 // 线程解锁 pthread_mutex_unlock(需要解除的锁的地址); // 返回值成功 0失败 错误码 // 销毁互斥锁 pthread_mutex_destroy(需要销毁的锁的地址); // 返回值成功 0失败 错误码控制顺序信号量可用于控制线程或进程的执行顺序库为 semaphore.h。sem_t 信号资源名称; // 为相应线程/进程定义信号资源信号资源sem_init(信号资源名称的地址, 0 或 !0, 0 或 1); // 第二个参数0 表示线程信号!0 表示进程信号 // 第三个参数0 表示没有资源时保持阻塞1 表示拥有资源继续执行返回值成功返回 0失败返回 -1用于初始化信号资源。申请资源sem_wait(信号资源名称的地址);资源为 1 时先申请后减 1资源为 0 时保持阻塞等待资源变为非 0。返回值成功返回 0失败返回 -1。释放资源sem_post(信号资源名称的地址); // 资源加 1不会阻塞返回值成功返回 0失败返回 -1。销毁资源sem_destroy(信号资源名称的地址); // 销毁资源变量返回值成功返回 0失败返回 -1。死锁死锁的产生需要同时满足四个条件互斥、不可剥夺、请求保持、循环等待。五、进程通信同一主机内传统方式无名管道、有名管道、信号。System V 方式共享内存、消息队列、信号量。不同主机间不同主机之间通过网络通信实现进程间数据交换。无名管道无名管道用于具有亲缘关系的进程间通信。int 管道名称[2]; pipe(管道名称); // 库unistd.h[0] 为读端[1] 为写端返回值成功返回 0失败返回 -1 并设置 errno。使用要点pipe 会在内核中创建一条管道不需要的读端或写端可通过close(管道名称[0/1])关闭。管道是半双工的同一时刻只能单向传输遵循 FIFO 队列先进先出读走即删除不支持 lseek 定位。I/O 操作包括 open很少应用、read、write、close库为 unistd.h。容量Linux 下实际大小约为 64KB超出后可能触发管道破裂。读端存在时若写满则阻塞若为空则可写读端关闭后写操作会直接异常终止程序触发 SIGPIPE 信号。写端存在时有数据则读取为空则阻塞写端关闭后有数据则读完为空则 read 返回 0不阻塞。若要实现全双工通信需要使用两个管道。有名管道有名管道可用于任意两个进程之间的通信本质是存在于系统内核中的特殊文件使用步骤为打开文件、读写、关闭。mkfifo(管道名称, 创建权限(0666)); // 库sys/types.h 和 sys/stat.h返回值成功返回 0失败返回 -1 并设置 errno。使用要点创建有名管道时需确认errno ! EEXIST该宏表示文件已存在否则会报错。若只打开管道的一端则会阻塞直至另一端以不同类型的 open 打开后解除阻塞若使用open(文件名, O_RDWR)则不会阻塞。读端open(文件名, O_RDONLY)写端open(文件名, O_WRONLY)O_RDWR 表示可读可写。删除文件unlink(文件名)库为 unistd.h。信号处理signal(信号, SIG_DFL / SIG_IGN / 自定义信号处理函数名称); // 库signal.h返回值成功返回上一次的处理函数失败返回 SIG_ERR。SIG_DFL默认处理终止、忽略、core dump。SIG_IGN忽略信号。func_ptr自定义信号处理函数。signal 用于注册信号处理函数以应对信号产生的结果。其中 SIGCHLD 信号表示子进程状态发生改变可在自定义函数中调用 wait / waitpid实现不阻塞主进程即可回收资源。例如void 函数名(int signo) { // 处理逻辑 }kill(pid号, 信号); // 库sys/types.h 和 signal.h返回值成功返回 0失败返回 -1用于向指定 pid 号的进程发送信号。例如kill(pid, SIGKILL); // 让该 pid 号的进程结束 kill(getpid(), SIGKILL); // 结束自己所在的进程pause(); // 库unistd.h用于等待信号返回值成功时表示捕获信号的函数成功取得信号失败返回 -1。共享内存共享内存类似管道但数据无需多次复制因此效率更高。获取共享内存的 key 值key_t KEY ftok(文件路径, 项目标识符); // 库sys/types.h 和 sys/ipc.h项目标识符低八位有效ftok 函数将路径名和项目标识符转换成一个 key 值用于标识共享内存段。返回值成功返回 key_t 类型值失败返回 -1 并设置 errno。创建共享内存int 共享内存ID shmget(KEY, 共享内存大小, 创建权限); // 库sys/ipc.h权限中若添加 IPC_CREAT 则创建共享内存添加 IPC_EXCL 则表示若共享内存已存在则返回错误。返回值成功返回共享内存 ID失败返回 -1 并设置 errno。进程通信方式对比通信方式通信方向是否亲缘关系数据量是否阻塞典型应用场景无名管道半双工单向需两个管道实现全双工仅亲缘关系父子进程小Linux 默认 64KB读写端存在时写满或读空会阻塞父子进程间简单数据传递、命令管道有名管道半双工单向任意进程小打开一端会阻塞直至另一端打开无亲缘关系的本地进程间通信信号单向异步通知任意进程极小仅信号编号非阻塞异步进程控制、异常通知、终止进程共享内存双向读写同一块内存任意进程大非阻塞需配合信号量同步大数据量、高频数据交换消息队列双向按消息类型读写任意进程中等读空或写满时可能阻塞进程间结构化消息传递、解耦通信信号量单向计数同步任意进程极小计数值资源不足时阻塞等待进程/线程同步、互斥访问共享资源六、映射共享内存地址到进程的私有空间void* 共享内存地址 shmat(共享内存ID, NULL, 读写权限); // 库sys/ipc.h // 第二个参数填 NULL 表示由系统指定地址返回值成功返回共享内存地址失败返回 -1 并设置 errno。判断映射是否成功时需要将返回值强转为(void *)-1进行比较。解除共享内存映射shmdt(共享内存地址); // 库sys/ipc.h返回值成功返回 0失败返回 -1 并设置 errno。共享内存地址必须使用 void* 类型的原地址推荐将源地址保存下来或直接用所需类型的地址变量承接该地址。删除共享内存shmctl(共享内存ID, IPC_RMID, NULL); // 库sys/ipc.h第三个参数填 NULL 表示不使用返回值成功返回 0失败返回 -1 并设置 errno。
阅读完成 · 觉得有帮助?
咨询建站