线程进程.

进程控制

  • fork 之后,父、子进程,共同争夺cpu ,执行先后顺序随机。

  • ps aux | grep 关键字。 —— 搜索系统中包含关键字的进程。

fork后父子进程异同

“刚刚” fork 后:

父子相同:

  • 全局变量、.data、.text、栈、堆、环境变量、用户ID、进程工作目录、宿主目录、信号处理方式...

父子不同:

  • 进程ID、fork返回值、进程运行时间、父进程ID、闹钟(定时器)、未决信号集。

读时共享,写时复制

  • fork后,对于父进程的用户空间的数据,系统采用 ==读时共享,写时复制== 原则。

  • 重点常用:全局变量。

fork后父子进程共享

  1. 文件描述符(对应 打开的文件结构体)

  2. mmap 创建的映射区。

进程回收

  • fork后的子进程,其父进程有义务在子进程结束时,回收该子进程pcb。隔辈进程无回收关系。

  • 进程终止:

    1. 关闭所有文件描述符

    2. 释放用户空间分配的内存。

    3. 进程的 pcb 残留在内核。保存进程结束的状态(正常:退出值。异常:终止其运行的信号编号)

孤儿进程

父进程,先于子进程终止。子进程沦为 “孤儿进程”。会被 init 进程领养。

使用命令:ps ajx 查看 ppid(父进程id)pid(进程id)gid(进程组id)sid(会话id)

僵尸进程

子进程终止,父进程未终止,但尚未对子进程回收。在此期间,子进程为 “僵尸进程”。

杀死进程命令:kill -9 进程id 。 只能杀死活跃的进程,对僵尸进程无效!!!

wait回收

  • 只有 “父、子” 进程之间存在,回收关系。 爷孙进程、兄弟进程、叔侄进程... 不存回收关系!

#include <sys/wait.h>
​
pid_t wait(int *wstatus);
参:  
    传出参数。回收进程的状态。传 NULL,只回收进程的pcb,不获取退出状态。
返回值:
    成功:回收的进程pid
    失败:-1, errno
  • 函数的作用:

    1. 阻塞等待子进程退出(终止)。

    2. 回收子进程残留在内核的 pcb。

    3. 获取子进程的退出状态(正常、异常)。—— 传出参数 :wstatus

  • 回收子进程退出状态:

    • 正常退出:

      • 判断 WIFEXITED(status) 为真。

      • 进一步使用 WEXITSTATUS(status) 获取退出值。

    • 异常退出:

      • 判断 WIFSIGNALED(status) 为真。

      • 进一步 使用 WTERMSIG(status) 获取杀死子进程的信号的编号。

waitpid回收

pid_t waitpid(pid_t pid, int *wstatus, int options);
参数:
    pid:
        >0: 通过pid指定 回收某一个子进程。
        -1: 回收任意子进程。
        0: 回收 与父进程属于同一个进程组的 子进程。
                —— 子进程创建成功后,默认,会自动加入父进程进程组。
                
    wstatus:(传出)回收子进程状态。 --- 类似于 wait() 的参数。
    options:WNOHANG —— 指定回收方式为 “非阻塞”。
             0 —— 指定回收方式为 “阻塞”。等同于 wait()
返回值:
    > 0: 表成功回收的进程pid
    0: 函数调用时参3指定了 WNOHANG,子进程没有结束。
    -1:失败。 errno    

进程间通信 IPC

  • 进程间通信的原理,借助 多个进程使用同一个 内核,借助内核,传递数据。

进程间通信的方法

  1. 管道:最简单。

  2. 信号:开销小。

  3. mmap映射:速度快,非血缘关系间。

  4. socket(本地套接字):稳定性好!

管道pipe

  • 实现原理:Linux 内核 使用环形队列机制,借助缓冲区(4k)实现。

  • 特质:

    1. 本质:伪文件(实为内核缓冲区)

    2. 用于进程间通信,右两个文件描述符引用,一个读端,一个写端。

    3. 规定,数据从管道写端流入,从读端流出。

  • 局限性:

    1. 自己写,不能自己读。

    2. 管道中的数据,读走就没!不能反复读取!

    3. 半双工通信。(对讲机)

    4. 应用于血缘关系进程间。

pipe管道读写行为

  • 读管道:

    1. 管道有数据, read返回实际读到的字节数。

    2. 管道无数据,1)无写端,read 返回 0 (类似读到文件末尾)

      2)有写端,阻塞等待。

  • 写管道:

    1. 无读端,异常终止。 ( SIGPIPE 信号)

    2. 有读端,1)管道已满,阻塞等待。

      2)管道未满,返回实际写出的字节数。

pipe管道的优劣

  • 优点:简单。比信号、套接字、mmap 简单很多!

  • 缺点:

    1. 只能单向通信,实现双向通信,需要两个管道。

    2. 只能应用于父子、兄弟...(有公共祖先)进程间同。无血缘关系进程间,后来用 fifo 替代。

命名管道 fifo

  • 命令创建:mkfifo 管道名。

  • 函数创建:mkfifo

    int mkfifo(const char *pathname, mode_t mode);
  • 可以用于无血缘关系进程间通信。 —— 演示代码:fifo_w.c fifo_r.c

  • 管道中的数据,一次性读取,读走没。

  • 读端:以 O_RDONLY 打开 fifo 管道。

  • 写端:以 O_WRONLY/O_RDWR 打开同一个 fifo 管道。

mmap

文件进程间通信

  • 有血缘关系、无血缘关系的进程,都可以使用 ==同一个文件== 来实现进间通信。

建立、释放映射区

  • mmap 借助文件映射,创建共享内存映射区。

mmap使用的注意事项

  1. 用于创建映射区的文件的大小,必须是非0。映射区的大小 <= 文件大小。

  2. 创建映射区,需要read权限。指定访问权限为 MAP_SHARED, mmap需要读写权限。 应该 <= 文件打开权限。只写不行!

  3. 文件描述符fd, 在mmap创建映射区完成,可以立即关闭!后续访问文件使用 内存地址。

  4. offset 必须是 4096 的整数倍。(MMU映射的最小单位 4k)

  5. 映射区访问的权限设为 MAP_PRIVATE, 对内存做的所有修改,都只在内存有效,不反应的磁盘上。不能应用于 IPC

进程间通信

父子进间通信

  1. 父进程 先创建 映射区。 open(O_RDWR); mmap(MAP_SHARED);

  2. fork() 创建子进程。

  3. 一个进程使用 映射区读,另一个进程写。

无血缘关系进程间通信

  1. 两个进程,打开同一个文件,创建映射区。

  2. 指定 flags 为 MAP_SHARED。

  3. 一个进程使用 映射区读,另一个进程写。

mmap特性

  • fifo、mmap 都可以应用于非血缘关系进程间通信。区别:

    • mmap:数据可以重复读写。

    • fifo:数据只能一次性读写。

  • 直接操作内存,执行速度快!

信号

信号描述

  • 信号的共性:

    1. 简单。

    2. 不能携带大量数据。

    3. 满足某一特定条件才发送。

  • 信号的特质:

    • 信号软件层面上的 “中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止,处理信号,处理结束后,再继续执行后续指令。

    • 所有的信号,产生、处理都是由内核完成。

    • 信号的实现手段导致,信号有很强的延时。对用户而言,依然感觉不到。

信号相关的概念

  • 未决:

    • 产生与递达(处理)之间的状态。 该状态主要受 阻塞(屏蔽)影响。

  • 递达:

    • 内核产生信号后递送并且成功到达进程。递达的信号,会被内核立即处理。

  • 信号处理方式:

    1. 执行默认动作。

    2. 忽略(丢弃)。

    3. 捕捉(调用用户指定的函数)。

  • 阻塞信号集(信号屏蔽字):

    • 本质:位图。用来记录信号的屏蔽状态。

    • 该信号集中的信号,表示成功被设置屏蔽。再次收到该信号,其处理动作将延后至解除屏蔽。此期间该信号一直处于未决态。

  • 未决信号集:

    • 本质:位图。用来记录信号的处理状态。

    • 该信号集中的信号,表示,信号已经产生,但尚未被处理。

信号4要素

  • 信号使用之前,==“必须”== 先确定该信号的 4 要素,再使用!

  • 四要素内容:

    1. 编号

    2. 名称

    3. 事件

    4. 默认处理动作 ( 参考讲义“Linux常规信号一览表” 或者 APUE 10.2 章)

  • 使用命令 kill -l 查看Linux 系统中,支持的所有信号。1~31 号:常规信号。

  • man 7 signal 查看信号 4 要素。

  • 9) SIGKILL 和19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。

信号的产生

  1. 按键产生

    • Ctrl + c → 2) SIGINT(终止/中断)

    • Ctrl + \ → 3) SIGQUIT(退出)

  2. 系统调用产生

    • alarm() → 14) SIGALRM

    • abort()

    • raise()

  3. 软件条件产生

    • alarm() → 14) SIGALRM

    • setitimer() → 14) SIGALRM(计时)

  4. 硬件异常产生信号

    • 段错误:内存访问异常 。→ 11) SIGSEGV

    • 浮点数例外:除 0。 → 8) SIGFPE

    • 总线错误。内存对齐出错。 → 7) SIGBUS

  5. 命令产生。

    • kill 命令

进程组和会话

  • 进程聚集成为进程组,多个进程组聚集成为会话。

守护进程

守护进程daemon概念

  • Linux后台的一些服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在后台运行。周期性的执行某种任务,或者等待某一事件的发生。 一般采用以d 结尾的命名方式。

线程

线程的概念

  • Linux 系统中,线程 称之为:轻量级的进程。

  • 进程:有独立的进程地址空间, 有独立的 pcb。 —— 最小资源分配单位。

  • 线程:有独立的pcb,没有独立的进程地址空间。(与其他线程共享) —— 最小执行单位。

  • 一个创建了线程的进程,本身也沦落 为线程。

  • LWP 号: cpu 划分时间片依据。 —— 线程 最小执行单位。

    • 查看LWP号命令: ps -Lf 进程pid

线程内核三级映射

  • 三级映射。—— 解释了,为什么线程没有独立的进程地址空间。

  • 《Linux内核源代码情景分析》 ----毛德操

线程共享

  • 独享:栈空间(用户栈、内核栈)errnum

  • 共享:./text ./ordata ./data ./bss heap堆 ----> 共享全局变量(可以创建堆区,能容直接共享到其它线程)

线程优缺点

  • 优点:

    • 并发性强。

    • 开销小。(共享一份地址空间)

    • 数据通信方便。(不需要IPC通信,因为共享全局变量和堆区)

  • 缺点:

    • 库函数,稳定性差。

    • 调试、编写困难(gdp不支持)

    • 对信号支持差。

  • 结论:既能使用进程开发,也能使用线程开发的程序,首选 线程。

线程同步

  • 同步:即协同步调,按预定的先后次序访问共享数据。

  • 线程同步,指一个线程发出某一功能调用时,没有得到结果之前,该调用不返回。同时,其他线程为保证数据一致性,不调用该功能。

  • 数据混乱产生的原因

    1. 资源共享

    2. 调度随机

    3. 线程之间缺乏必要同步机制。

  • 只能从第3步着手解决 “数据混乱”, 避免产生与时间有关的错误。执行多个控制流(线程)访问共享的数据的先后顺序。

  • 锁的属性:

    • 系统提供内用户使用的所有的锁,全部为 “建议锁”, 不具备强制性。

    • 如果访问共享数据的线程,不拿锁,直接访问共享数据,能访问成功。但是,数据就有出现混乱的风险。 —— 拿锁与否完全取决于编写的程序。

互斥量(互斥锁)

pthread_mutex_t mutex;  创建互斥锁
​
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);   初始化互斥锁
​
int pthread_mutex_lock(pthread_mutex_t *mutex); 加锁, 如果锁被占用,阻塞等。
​
int pthread_mutex_trylock(pthread_mutex_t *mutex); 尝试加锁,不阻塞。
​
int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁, 唤醒阻塞在此把锁上的线程。
​
int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁互斥锁
  • 注意事项:

    1. 保证锁的粒度 , 越小越好!( 访问共享数据前,加锁,访问共享数据结束,立即解锁。)

    2. 互斥锁,本质:结构体。 我们可以把它当成整数看待。 初值为 1(init成功)。 取值:1 或 0

    3. 加锁:--操作。1-- ---> 0 。 失败:阻塞线程。

    4. 解锁:++操作。0++ ---> 1。 成功:唤醒阻塞在锁上的线程。

    5. try锁:尝试加锁。 成功--。 失败,设置错误号 EBUSY、EINVAL

  • 初始化互斥量

    pthread_mutex_t mutex;
    ​
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 静态初始化。--- 常用于全局变量
    pthread_mutex_init(&mutex, NULL);   // 动态初始化。--- 常用于函数内部初始化。

死锁

  • 死锁不是一种锁!是错误使用锁的一种状态。

  • 常见死锁种类:

    1. 对一把锁,反复lock。

    2. 两个线程, 各自持有一把锁,请求另一把锁。

读写锁

3句话描述

  1. 锁,只有一把!可以指定为 “读模式” 和 “写模式”

  2. 读共享,写独占。

  3. 写锁优先级高!( 读锁已经加锁成功,写锁无法加锁成功,阻塞等。)

  • 相较于互斥锁而言,读写锁非常适合于对数据结构读的操作次数,远大于写的操作次数的场景。

信号量(semaphore)

  • 信号量与信号无关!

  • 信号量 相当于 初始值为 N 的互斥量。 N值,表示可以同时访问共享数据的线程数。

操作函数

#include <semaphore.h>
sem_t sem; // 创建信号量对象
int sem_init(sem_t *sem, int pshared, unsigned int value);  //动态初始化方法。没有静态
    参2 pshared:  0: 用于线程间同步
                 非0(1): 用于进程间同步
    参3 value:信号量的初值。 N值。代替同时访问共享数据的线程个数。                        返回值:
    成功:0
    失败:-1, errrno --- perror()
        
int sem_destroy(sem_t *sem);
​
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
           struct timespec {
               time_t tv_sec;      /* Seconds */
               long   tv_nsec;     /* 纳秒 Nanoseconds [0 .. 999999999] */
           };
    abs:绝对。
    绝对时间:从 1970年1月1日 00:00:00 开始,计算时间。
    
int sem_wait(sem_t *sem);
    一次调用,做一次--操作。当信号量的值为0时, 再次--,会阻塞。(对比pthread_mutex_lock)
    
int sem_post(sem_t *sem);
    一次调用,做一次++操作。当信号量的值为N时, 再次++,会阻塞。(对比pthread_mutex_unlock)
    
Link with -pthread.  // 编译时,指定线程库
​

生产者消费者模型

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值