线程与进程
进程:程序的一个执行实例,正在执行的程序,实际上就是分配系统资源(CPU时间,内存)的实体。查看进程/proc文件夹中查看。
线程:在一个程序中的一个执行路线叫做线程,也就是说线程是进程内部的一个控制序列。线程也被叫做轻量级进程(lwp)。
综上:进程是资源分配的基本单位,而线程是调度的基本单位,线程共享进程数据,但也拥有自己的一部分的数据,比如线程id,一组寄存器,栈,errno,信号屏蔽字,调度优先级。 对于线程共享的资源包括:定义的全局变量,文件描述符表,每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数,当前工作目录,用户id和组id。
进程
进程状态
执行:时间片轮转到该进程进行执行。
就绪:等该时间片结束之后进入就绪状态。
挂起:挂起主要是将暂不执行的进程换出到外存,节省内存空间。
阻塞:表示该进程正在等待一个事件的发生,阻塞状态下收到信号会切换到就绪状态。
挂起就绪:进程在外存中,但是只要被载入内存中就可以执行。
挂起阻塞:进程在外存中等待一个资源,即使被载入内存也无法被执行。
初始:初始化
退出:释放进程申请的一切资源进行退出。
Linux将进程的阻塞分为:暂停(stop),浅睡眠(sleep),深睡眠(disk sleep)。
进程的分类
1.僵尸进程:被终止但是资源还没被回收的进程,子进程退出,而父进程并没有调用wait或者waitpid来回收资源,就会产生僵尸进程,就会产生僵尸进程。僵尸进程的进程描述符仍然留在系统的进程表中。
2.孤儿进程:父进程先结束,子进程就会称为孤儿进程,会被init进程所接管,并由init进程调用wait等待其结束。
3.守护进程:在后台执行的程序,会以进程的形式初始化。创建守护进程:1.在父进程中fork(),并退出父进程,在子进程中调用setsid()创建新的会话;3.修改工作目录为“/”;4.关闭不需要的文件描述符,指的是fork()的时候,被子进程继承的父进程的没用的文件描述符;5.设置进程的umsak为0.
进程/CPU调度算法
时间⽚轮转(Round Robin,RR):优点:没有饥饿问题。问题:若时间⽚⼩,进程切换频繁,吞 吐量低;若时间⽚⻓,则响应时间过⻓,实时性得不到保证。
进程间通信
管道
匿名管道
int pipe(int fd[2]);
//fd:文件描述符数组,fd[0]表示读端,fd[1]表示写端
管道通常是父子进程之间进行通信的,需要从父进程进行读取,则需要关闭父进程的写端口,子进程进行写入,需要关闭子进程的读端口。这里调用的读写函数是read和write
#include <iostream>
#include <cstring>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
//父进程进行读取,子进程进行写入
int main()
{
int fds[2]; //用来接受创建管道中的输出型参数
int d = pipe(fds);
assert(d==0);
int id=fork();
assert(id>=0);
if(id==0)
{
//子进程进行写入,关闭读取
close(fds[0]);
//子进程
int cnt=0;
const char* s= "这是子进程给父进程发送消息";
while(true)
{
cnt++;
char buffer[1024];
//c语言的格式化字符串的输出
snprintf(buffer,sizeof buffer,"child-->parent: %s[%d][%d]",s,cnt,getpid());
//将buffer的数据通过系统调用write写到管道中
write(fds[1],buffer,strlen(buffer));
if(cnt==5)
break;
//sleep(1);
}
close(fds[1]);
exit(0);
}
//父进程进行读取,关闭写入
close(fds[1]);
while(true)
{
char buffer[1024];
//调用系统调用read从管道文件中读取字符
ssize_t n= read(fds[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]=0;
cout<<"#"<<buffer<<"| parent pid"<<getpid()<<endl;
}
else if(n==0) //当写入操作结束的时候,读取到最后一个字符的时候,可以退出
{
cout<<"管道内的内容已经被读取完成"<<endl;
break;
}
}
//父进程,需要等待子进程
int status=0;
int n=waitpid(id,&status,0);
//status的低7位是进程退出码
cout<<"pid-->"<<(status&0x7F)<<endl;
assert(n==id);
//[0] 读相当于一只嘴巴
//[1] 写 相当于一只钢币
// cout<<"fds[0]:"<<fds[0]<<endl;
// cout<<"fds[1]:"<<fds[1]<<endl;
//std::cout<<"hello c++"<<std::endl;
return 0;
}
如果所有管道写端对应的文件描述符被关闭,则read返回0,如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
匿名管道的特点:1.通常是父子间进程进行通信的手段,或者共同祖先的进程之间进行通信,一个管道由该进程创建,然后该进程fork()。2.进程退出,管道释放,管道的声明周期随进程。3.内核会对管道操作同步和互斥。4.管道是半双工的,需要全双工的话,需要开两个管道。
命名管道
创建一个有名字的文件,使用
mkfifo filename
//也可以从函数里面创建
int mkfifo(const char* filename,mode_t mode)
对于命名管道的打开用的是open,匿名管道用的是pipe()。
//Client.cpp
#include "Comm.hpp"
//这边是发送信息
int main()
{
int wrd = open(NAMED_PIPE,O_WRONLY);
if(wrd<0) exit(-1);
char buffer[1024];
while(true)
{
cout<<"Please say# ";
fgets(buffer,sizeof(buffer),stdin); //先往字符串写入字符
if(strlen(buffer)>0) buffer[strlen(buffer)-1]=0; //把最后一个字符改成/0
ssize_t n =write(wrd,buffer,strlen(buffer));
assert(n==strlen(buffer));
(void)n;
}
close(wrd);
return 0;
}
//server.cpp
#include "Comm.hpp"
//这边是接受信息,文件的创建和删除都在这边
int main()
{
bool n = creatFifo(NAMED_PIPE);
assert(n);
(void)n;
//开始从命名管道读数据
int rfd = open(NAMED_PIPE,O_RDONLY);
if(rfd<0) exit(-1);
char buffer[1024];
while(true)
{
ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"client->server# "<<buffer<<endl;
}
else if(s==0)
{
cout<<"client quit,me too!"<<endl;
break;
}
else{
cout<<"err string:"<<strerror(errno)<<endl;
break;
}
}
remove(NAMED_PIPE);
return 0;
}
//comm.hpp
#include <iostream>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <assert.h>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
#define NAMED_PIPE "/tmp/Namedpipe"
bool creatFifo(const string &path)
{
umask(0); //首先设置umask的值为0
int n=mkfifo(path.c_str(),0666); //创建的文件的权限是 rw
if(n==0)
return true;
else
{
std::cout<<"errno: "<<errno <<"err string :" <<strerror(errno)<<endl;
return false;
}
}
void removeFifo(const string &path)
{
int n = unlink(path.c_str());
assert(n==0); //debug中的assert才有效,release模式下不执行assert
(void)n;
}
System V进程间通信
三种方式:System V消息队列 System V共享内存 System V信号量
共享内存函数
int shmget(key_t key, size_t size, int shmflg);
//功能:用来创建共享内存
//key:这个共享内存段名字
// size:共享内存大小
// shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
void *shmat(int shmid, const void *shmaddr, int shmflg);
//功能:将共享内存段连接到进程地址空间
//shmid: 共享内存标识
//shmaddr:指定连接的地址,为NULL,随机选择一个地址
//shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
int shmdt(const void *shmaddr);
//功能:将共享内存段与当前进程脱离
//shmaddr: 由shmat所返回的指针
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//功能:用于控制共享内存
//shmid:由shmget返回的共享内存标识码
//cmd:将要采取的动作(有三个可取值)
// buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <stdio.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x345
//共享内存的大小,一般建议是4KB的整数倍
//系统分配共享内存是以4KB为单位!----内存划分内存块的基本单位Page
//内核会给你向上取整 4097 ---4KB*2 内核给你的和你能用的是两码事,就是你申请4097的大小
//内核会给你4kb*2的大小
//虽然给了你4096*2的内存,但是你只能用4097
#define MAX_SIZE 4096
key_t Getftok(const char* pathname,int id)
{
key_t k= ftok(PATHNAME, PROJ_ID); //可以获取同一个KEY!!
if(k<0)
{
// cin, cout, cerr -> stdin, stdout, stderr -> 0, 1, 2
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1); //退出错误码
}
return k;
}
int ShmHelper(key_t key,int mode)
{
// key是shmget,设置进入共享内存属性中的!!!
//用来表示该共享内存再内核中的唯一性!!
int shmid = shmget(key,MAX_SIZE,mode);
if(shmid<0)
{
std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
exit(2);
}
return shmid;
}
//创建共享内存
int CreateShm(key_t key)
{
return ShmHelper(key,IPC_CREAT|IPC_EXCL|0666); //注意这里的0x666必须传,文件的权限,如果时0的画,就不能对共享内存文件进行操作
}
//获得贡献内存的shmid值
int GetShm(key_t key)
{
return ShmHelper(key,IPC_CREAT);
}
//让进程和shmid关联
void* AttachShm(int shmid)
{
void* start = shmat(shmid,nullptr,0); //再Linux系统中,是64位的系统,所以指针的大小是8字节
//如果与int 4字节相比的话,会有精度缺失
if((long long)start == -1L)
{
std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
exit(3);
}
return start;
}
//去关联
void DtachShm(void* shmaddr)
{
if(shmdt(shmaddr) == -1 )
{
std::cerr<<"DtachShm"<<errno<<":"<<strerror(errno)<<std::endl;
}
}
//删除共享资源块
void Delshm(int shmid)
{
if(shmctl(shmid,IPC_RMID,nullptr)==-1)
{
std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
}
}
#endif
消息队列
消息队列是消息组成的链表,保存在内核中。消息队列中的消息是⼀个具有特定格式的数据块。操作系 统中可以存在多个消息队列,每个消息队列有唯⼀的 key 进⾏标识。
信号量
信号量是⼀种特殊的变量,对它的操作都是原⼦的(屏蔽中断),有两种操作:V(signal())和 P (wait())。V 操作会增加信号量 S 的数值,P 操作会减少它。(信号量 S 的值相当于记录资源的个 数)
信号
信号是系统为响应某些条件而产生的一个事件,接收到该信号的进程可以采取OS事先定义好的行为。类似于ctrl+c结束进程
套接字Socket
不同主机之间的进程进行通信,也可以本地通信。
协程
协程是⼀个⽤户态的线程,⽤户在堆上模拟出协程的栈空间。当需要进⾏协程上下⽂切换的时候,主线程只需要交换栈空间和恢复协程的⼀些相关的寄存器的状态,就可以实现上下⽂切换。没有了从⽤户态 转换到内核态的切换成本,协程的执⾏也就更加⾼效。和传统的线程不同的是:线程是抢占式执⾏,当 发⽣系统调⽤或者中断的时候,交由OS调度执⾏;⽽协程是通过 yield 主动让出cpu所有权,切换到其他协程执⾏。
线程
线程间同步与互斥
同步与互斥:互斥锁,条件变量,信号量和读写锁。
信号量:允许多个线程同一时刻访问同一资源,但是需要限制在同⼀时刻访问此资源的最⼤线程数⽬,⼀般是将当前可⽤资源计数设置为最大资源计数,每增加⼀个线程对共享资源的访问,当前可⽤资源计数就会减1 ,只要当前可⽤资源计数是⼤于0 的,就可以发出信号。但是当前可⽤计数减⼩到0时则说明当前占⽤资源的线程数已经达到了所允许的最⼤数⽬,不能在允许其他线程的进⼊,此时的信号量信号将⽆法发出。
线程互斥中的共享变量
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,即串行访问临界资源,通常对临界资源起保护作用,通常是加互斥锁.
原子性:具有一次性做完没有任何能中断的机制,叫做原子性。
操作共享变量就需要加上互斥锁,不然在同一时间进入临界资源的代码会出现问题。再加上锁之后,就被称为临界区,只允许一个线程进入操作,当该线程离开之后,其余线程才能申请到锁,对临界资源进行访问。
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
// #include "PThread.hpp"
using namespace std;
int tickets=1000; //共享资源火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //该锁如果是局部的,或者静态的必须使用init进行初始化,结束后要destroy
void* getTick(void* args)
{
// 1. 多个执行流进行安全访问的共享资源 - 临界资源
// 2. 我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分
// 3. 想让多个线程串行访问共享资源 -- 互斥
// 4. 对一个资源进行访问的时候,要么不做,要么做完 -- 原子性 , 不是原子性的情况 -- 一个对资源进行的操作,如果只用一条汇编就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述
// 提出解决方案:加锁!
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换
std::string username = static_cast<const char *>(args);
while (true)
{
// 加锁和解锁的过程多个线程串行执行的,程序变慢了!
// 锁只规定互斥访问,没有规定必须让谁优先执行
// 锁就是真正的让多个执行流进行竞争的结果
std::string username = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&lock); //加锁
if (tickets > 0)
{
usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒
//调用usleep的时候,会从用户态转向内核态
std::cout << username << " 正在进行抢票: " << tickets << std::endl;
tickets--;
pthread_mutex_unlock(&lock); //加锁和解锁的过程只针对于对临界资源的访问,也就是临界取的代码
}
else
{
pthread_mutex_unlock(&lock);
break;
}
//抢完票就结束了嘛
usleep(10000); //形成一个订单给用户
}
return nullptr;
}
int main()
{
unique_ptr<Thread> thread1(new Thread(getTick,(void*)"usr1",1));
unique_ptr<Thread> thread2(new Thread(getTick,(void*)"usr2",2));
unique_ptr<Thread> thread3(new Thread(getTick,(void*)"usr3",3));
unique_ptr<Thread> thread4(new Thread(getTick,(void*)"usr4",4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
线程同步中的条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
//条件变量的定义
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include "task.hpp"
using namespace std;
const int _max = 500;
template <class T>
class blockqueue
{
public:
blockqueue(const int maxnum = _max):_cap(maxnum)
{
// 完成锁和条件变量的初始化
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
//生产者产生数据之后,放入到这里面
void _push(const T &in) // 输入型参数 const&
{
pthread_mutex_lock(&_mutex);
//1、判断
//细节2:充当条件判断必须是while,不能用if
//当你被唤醒的时候,可能存在异常或者伪唤醒的情况,pthread_cond_broadcast()把所有线程唤醒的话,如果此时只有一个位置,不进行判断的话,后面就容易出错。
while(is_full()) //如果满了的话,就得等待
{
//细节1,pthread_cond_wait()这个函数的第二个参数必须是我们使用的互斥锁!
//a、pthread_cond_wait:该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起
//b、pthread_cond_wait:该函数被唤醒返回的时候,会自动的重新获取你传入的锁
pthread_cond_wait(&_pcond,&_mutex); //因为生产条件不满足,无法生产,此时我们的生产者进行等待
}
//当运行到这里的时候,说明队列是不满的状态
_q.push(in); //将data数据存储进去
//细节3:这个函数可以在临界区内部,也可以放在外部
pthread_cond_signal(&_ccond);
pthread_mutex_unlock(&_mutex);
}
//主要作用是在消费者进行消费之后,弹出对应的数据
void _pop(T *out) // 输出型参数
{
pthread_mutex_lock(&_mutex);
//1、判断
while(is_empty())
{
pthread_cond_wait(&_ccond,&_mutex);
}
//2、程序走到这里一定不会空
*out=_q.front();
_q.pop();
//3、绝对保证,阻塞队列里面至少有一个空的位置
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_mutex);
}
~blockqueue()
{
pthread_cond_destroy(&_ccond);
pthread_cond_destroy(&_pcond);
pthread_mutex_destroy(&_mutex);
}
private:
int is_full()
{
return _q.size()==_cap;
}
int is_empty()
{
return _q.empty();
}
private:
int _cap; // 队列能够容纳的最大数量
queue<T> _q;
pthread_mutex_t _mutex; // 互斥锁
pthread_cond_t _ccond; // 消费者的条件变量
pthread_cond_t _pcond; // 生产者的条件变量
};
信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include <cassert>
const int getcap = 5;
template <class T>
class ring_queue
{
private:
// P操作主要是对数据量进行预定
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
//主要是对信号量进行还原
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
ring_queue(const int cap = getcap) : _queue(cap), _cap(cap)
{
// 初始化信号量
int n = sem_init(&_productor_sem, 0, _cap); // 初始化信号量,注意这里对生产者数量的初始化时剩余的可以放置任务线程的个数
assert(n == 0);
(void)n;
n = sem_init(&_comsumer_sem, 0, 0); //初始化消费者生产量,注意这里是对消费者可以消费的数量进行初始化,初始应该为0
assert(n == 0);
(void)n;
//对消费者和生产者的位置进行初始化
_spacePostion = _dataPostion = 0;
//对锁进行初始化,这里需要两把锁,对于 生产者和消费者而言,他们是互斥的
pthread_mutex_init(&_productor_mutex,nullptr);
pthread_mutex_init(&_comsumer_mutex,nullptr);
}
void Push(const T &in)
{
// 主要是进行PV操作,首先需要明确的是PV操作是原子性的
//首先对资源进行预定
P(_productor_sem);
//对这里进行加锁的时候需要注意,在P操作之后加锁,在V操作之前解锁
//因为PV操作是原子性的,所以可以不用加锁,而且加锁的话会影响其余线程提前对资源的预定,浪费时间
//如果在P操作之前加锁,那么必须等这个线程在释放锁之后,其余线程才能申请资源在进行后续操作 ,比较浪费时间
//生产者消费者模型节约时间主要是在 在该生产者线程进行操作的时候,其余线程可以先去预定资源,申请任务,以及后续的在任务执行完成之后,并行完成后续的一系列操作
pthread_mutex_lock(&_productor_mutex);
_queue[_spacePostion++]=in;
_spacePostion%=_cap;
pthread_mutex_unlock(&_productor_mutex);
V(_comsumer_sem); //消费者可以消费的资源+1
}
void Pop(T *out)
{
//主要是对消费者进行PV操作,首先需要注意的是这里需要对消费者P操作,生产者V操作
//首先消费者对资源进行消费预定
P(_comsumer_sem);
pthread_mutex_lock(&_comsumer_mutex);
*out = _queue[_dataPostion++];
_dataPostion%=_cap;
pthread_mutex_unlock(&_comsumer_mutex);
V(_productor_sem); //生产者可以生产的资源+1
}
~ring_queue()
{
// 销毁信号量
sem_destroy(&_productor_sem);
sem_destroy(&_comsumer_sem);
//对锁进行销毁
pthread_mutex_destroy(&_productor_mutex);
pthread_mutex_destroy(&_comsumer_mutex);
}
private:
std::vector<T> _queue;
int _cap;
sem_t _productor_sem;
sem_t _comsumer_sem;
int _spacePostion;
int _dataPostion;
pthread_mutex_t _productor_mutex;
pthread_mutex_t _comsumer_mutex;
};
死锁
死锁的四个必要条件:互斥条件:一个资源每次只能被一个执行流使用,串行访问;2.请求与保持条件:一个执行流因为申请资源而阻塞的时候,对已经获得资源保持不放;3.不剥夺条件:一个执行流已经获得的资源在未使用完之前,不能强行剥夺;4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。