进程与线程 -- C/C++(二)

简介: 进程:进程有独立的地址空间Linux为每个进程创建task_struct每个进程都参与内核调度,互不影响

线程

进程:


进程有独立的地址空间


Linux为每个进程创建task_struct


每个进程都参与内核调度,互不影响


线程:


进程在切换时候系统开销比较大


很多操作系统引入了轻量级LWP


同一个进程中的线程共享相同的地址空间


Linux不区分进程、线程


特点:


通常线程是指共享相同地址空间的多个任务


使用线程的好处


●  大大提高了任务切换效率

●  避免了额外的TLB & cache的刷新


一个进程中的线程共享以下的资源


●  可执行的指令

●  静态数据

●  进程中打开的文件描述符

●  当前工作目录

●  用户ID

●  用户组ID


Pthread线程库

1.pthread 线程库提供了如下基本操作

  • 线程创建
  • 线程回收
  • 结束线程

2.同步和互斥机制

  • 信号量
  • 互斥锁


线程创建 - pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*routine)(void *), void *arg);

●  成功时候返回0, 失败时返回错误码

●  thread是线程对象

●  attr线程属性,NULL代表默认属性

●  routine线程执行的函数

●  传递给routine的参数


线程回收- pthread_join

int pthread_join(pthread_t thread, void **retval);

●  成功时候返回0, 失败时返回错误码

●  thread是要回收的线程对象

●  调用线程阻塞知道thread结束

●  *retval接收线程thread的返回值


线程结束- pthread_exit

void pthread_exit(void *retval);

●  结束当前线程

●  retval可被其他线程通过pthread_join获取

●  线程私有资源被释放

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <sys/stat.h>
#include <pthread.h>
#include <string.h>
char message[32] = "hello world";
void* thread_func(void *arg);
int main()
{
    pthread_t a;
    void *result;
    if(pthread_create(&a, NULL, thread_func, NULL) != 0)
    {
        perror("error");
        return -1;
    }
    pthread_join(a, &result);
    printf("result is %s\n", result);
    printf("message is %s\n", message);
}
void* thread_func(void *arg)
{
    sleep(1);
    strcpy(message, "marked by thread");
    pthread_exit("aaa");
}

线程间通信

线程共享统一进程的地址空间


优点: 线程间通信很容易,通过全局变量交换数据


缺点:多个线程访问共享数据时需要同步或者互斥机制


线程通信 - 同步

●  同步指的是多个任务按照约定的先后顺序相互配合完成一件事情

●  信号量

●  信号量来决定线程是继续运行还是阻塞


信号量

信号量代表某一类资源,其值表示系统中该资源的数量


信号量是一个受保护的变量,只能通过三种操作来访问


●  初始化

●  P操作(申请资源)

●  V操作(释放资源)


Posix信号量

posix中定义了两类信号量


●  无名信号量(基于内存的信号量)

●  有名信号量(线程之间和进程之间)


pthread库常用的信号量操作函数如下:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, ussigned int value);
int sem_wait(sem_t *sem); // p操作
int sem_post(sem_t *sem); // v操作

信号量初始化 - sem_init

int sem_init(sem_t *sem, int pshared, unsigned int val);

成功时候返回0, 失败时候返回EOF


sem 指向要初始化的信号量对象


pshared 0 - 线程间 1 - 进程间


信号量P/V操作

int sem_wait(sem_t *sem); // p操作
int sem_post(sem_t *sem); // v操作

成功时候返回0, 失败时候返回EOF


sem 指向要操作的信号量对象


信号量代码示例:

#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <semaphore.h>
#include <pthread.h>
char buf[32];
sem_t sem;
void* function(void *arg);
int main()
{
    pthread_t a_thread;
    if(sem_init(&sem, 0, 0) < 0)
    {
        perror("sem_init");
        return -1;
    }
    if(pthread_create(&a_thread, NULL, function, NULL) < 0)
    {
        printf("failed to pthread_create");
        return -1;
    }
    printf("input quit to exit");
    do
    {
        fgets(buf , 32, stdin);
        sem_post(&sem);
    }while(strncmp(buf, "quit", 4) !=0 );
    return 0;
}
void* function(void *arg)
{
    while(1)
    {
        sem_wait(&sem);
        printf("you enter %d cahraters\n", strlen(buf));
        fflush(stdout);
    }
}

使用两个信号量做到数据严格同步:

#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <semaphore.h>
#include <pthread.h>
char buf[32];
sem_t sem_r, sem_w;
void* function(void *arg);
int main()
{
    pthread_t a_thread;
    if(sem_init(&sem_r, 0, 0) < 0)
    {
        perror("sem_init");
        return -1;
    }
    if(sem_init(&sem_w, 0, 1) < 0)
    {
        perror("sem_init");
        return -1;
    }
    if(pthread_create(&a_thread, NULL, function, NULL) < 0)
    {
        printf("failed to pthread_create");
        return -1;
    }
    printf("input quit to exit");
    do
    {
        sem_wait(&sem_w);
        fgets(buf , 32, stdin);
        sem_post(&sem_r);
    }while(strncmp(buf, "quit", 4) !=0 );
    return 0;
}
void* function(void *arg)
{
    while(1)
    {
        sem_wait(&sem_r);
        printf("you enter %d cahraters\n", strlen(buf));
        fflush(stdout);
        sem_post(&sem_w);
    }
}

线程通信 - 互斥

●  临界资源
一次只允许一个任务(进程、线程)访问的共享资源

●  临界区
访问临界区的代码

●  互斥机制
mutex互斥锁
任务在访问临界资源前申请锁,访问完之后释放锁


互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_init:

成功时候返回0,失败时候返回错误码


mutex 指向要初始化的互斥锁对象


attr 互斥锁属性,NULL表示缺省属性


pthread_mutex_lock:

成功时候返回0,失败时候返回错误码


mutex 指向要初始化的互斥锁对象


如果无法获得锁,任务阻塞


pthread_mutex_unlock:

成功时候返回0,失败时候返回错误码


mutex 指向要初始化的互斥锁对象


执行完临界区要及时释放锁

#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <semaphore.h>
#include <pthread.h>
unsigned int count, value1, value2;
pthread_mutex_t lock;
void* function(void *arg);
#define LOCKM_
int main()
{
    pthread_t a_thread;
    if(pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("pthread_mutex_init");
        return -1;
    }
    if(pthread_create(&a_thread, NULL, function, NULL) != 0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        count++;
#ifdef LOCKM_
        pthread_mutex_lock(&lock);
#endif
        value1 = count;
        value2 = count;
#ifdef LOCKM_
        pthread_mutex_unlock(&lock);
#endif
    }
    return 0;
}
void* function(void *arg)
{
    while(1)
    {
#ifdef LOCKM_
        pthread_mutex_lock(&lock);
#endif
        if(value1 != value2)
        {
            printf("value1 = %d , value2 = %d\n", value1, value2);
            fflush(stdout);
            usleep(1000);
        }
#ifdef LOCKM_
        pthread_mutex_unlock(&lock);
#endif
    }
}

进程间通信

●  早期UNIX进程间通信方式
无名管道(pipe)
有名管道(fifo)
信号(signal)

●  System V IPC
共享内存(share memory)
消息队列(message queue)
信号灯集(semaphore set)

●  套接字(socket)


无名管道

●  只能用于具有亲缘关系的进程之间的通信

●  单工通信模式,具有固定的读端和写端

●  无名管道创建时候会返回两个文件描述符,分别用于读写管道

int pipe(int pfd[2]);

pipe:

成功时候返回0,失败时候返回EOF


pfd包含两个元素的整形数组,用于保存文件描述符


pfd[0]用于读管道,pfd[1]用于写管道

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    pid_t pid1, pid2;
    char buf[32];
    int pfd[2];
    if(pipe(pfd) < 0)
    {
        perror("pipe");
        return -1;
    }
    if((pid1 = fork()) <0)
    {
        perror("fork");
        return -1;
    }
    else if(pid1 == 0)
    {
        strcpy(buf, "im process1");
        write(pfd[1], buf, 32);
        exit(0);
    }
    else
    {
        if((pid2 == fork()) < 0)
        {
            perror("fork2");
            return -1;
        }
        else if(pid2 == 0)
        {
            sleep(1);
            strcpy(buf, "im process2");
            write(pfd[1], buf, 32);
        }
        else
        {
            wait(NULL);
            read(pfd[0], buf, 32);
            printf("%s\n", buf);
            wait(NULL);
            read(pfd[0], buf, 32);
            printf("%s\n", buf);
        }
    }
    return 0;
}

读无名管道

●  写端存在
有数据 – read返回实际读取的字节数量
无数据 – 进程读阻塞

●  写端不存在
有数据 – read返回实际读取的字节数量
无数据 – read返回0


写无名管道

●  读端存在
有空间 – write返回实际写入的字节数
无空间 – 空间不足时候不保证原子操作(有多少写多少,剩余等有空间再写),进程写阻塞

●  读端不存在不允许写入
管道断裂


有名管道

对应管道文件,可用于任意进程之间通信


打开管道时候可以指定读写方式


通过文件I/O操作,内容存放在内存中

int mkfifo(const char *path, mode_t mode);

mkfifo:

成功时候返回0,失败返回EOF


path 创建管道文件路径


mode 管道文件权限,如0666


信号

●  信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

●  linux内核通过信号通知用户进程,不同的信号类型代表不同的事件

●  Linux对早期unix信号机制进行了扩展

●  进程对信号有不同的响应方式
缺省方式
忽略信号
捕捉信号


常用的信号

信号名 含义 默认操作
SIGHUP 该信号在用户终端关闭时候产生,通常是发送给和该终端关联的会话内所有的进程 终止
SIGINT 该信号在用户键入INTR制度时候产生,内核发送此信号到当前终端的所有前台进程(ctrl+c) 终止
SIGQUIT 该信号和sigint类似,但是由于quit字符来产生(ctrl+\) 终止
SIGLL 该信号在一个进程企图执行一条非法指令时候产生 终止
SIGSEV 该信号在非法访问内存时候产生,如野指针、缓冲区溢出 终止
SIGPIPE 当进程往一个没有读端的管道中写入时产生,代表管道断裂 终止
SIGKILL 该信号用来结束进程,并且不能被捕捉和忽略 终止
SIGSTOP 该信号用于暂停继承,并且不能被捕捉和忽略 暂停进程
SIGTSTP 该信号用于暂停继承,用户可以键入SUSP字符发出这个信号(ctrl+z) 暂停进程
SIGCONT 该信号让进程进入运行态 继续运行
SIGALRM 该信号用于通知进程定时器时间已到 终止
SIGUSR1/2 该信号保留给用户程序使用 终止

信号发送

int kill(pid_t pid, int sig);
int raise(int sig);  
int alarm(unsigned int seconds); // 创建一个定时器,如果为0取消当前定时器
int pause(void);  // 进程会一直阻塞,知道被信号终端


设置信号的响应方式

void (*signal(int signo, void(*handker)(int)))(int);

System V IPC

包括共享内存,消息队列和信号灯集


每个IPC对象有唯一的ID


IPC对象创建后一直存在,知道被显式的删除


每一个IPC对象有一个关联的key

key_t ftok(const char *path, int proj_id);

ftok:

成功时候返回key,失败返回EOF


path是存在可访问的文件路径


proj_id是用于生成key的数字,不能是0

#include <stdio.h>
#include <stdli.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
int main()
{
  key_t key;
    if((key=ftok(".", 'a')) == -1)
    {
        perror("ftok");
        return -1;
    }
}

共享内存

●  共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据拷贝

●  共享内存在内核空间创建,可以被进程映射到用户空间访问,使用灵活

●  由于多个进程可以同时访问共享内存,因此需要同步和互斥机制配合使用


使用步骤

1.创建/打开共享内存

2.映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

3.读写共享内存

4.撤销共享内存映射

5.删除共享内存对象

// 1. 创建/打开共享内存
int shmget(kety_t key, int size, int shmflg);
// 2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 3. 读写共享内存
...
// 4. 撤销共享内存映射
int shmdt(void *shmaddr);
// 5. 设置共享内存, 删除共享内存对象
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmget:

key 和共享内存关联的key, IPC_PRIVATE或者ftok生成


shmflg 共享内存标志位 IPC_CREATE | 0666


示例代码(不能直接运行)

// 创建私有
int main()
{
  int shmid;
  if((shmid = shmget(IPC_PRIVATE, 512, 0666) < 0)
  {
        perror("shmget");
        return -1;
  }
}
// 创建关联key的
int main()
{
    key_t key;
  int shmid;
    if((key = ftok('.', 'm') == -1)
    {
        perror("ftok");
        return -1;
    }
  if((shmid = shmget(key, 1024, IPC_CREEATE|0666) < 0)
  {
        perror("shmget");
        return -1;
  }
}     

shmat:

shmid 要映射的共享内存ID


shmaddr 映射后的地址,NULL表示由系统自动映射


shmflg 标志位, 0表示可读可写,SHM_RDONLY表示只读


    ●  通过指针访问共享内存,指针类型取决于共享内存存放的数据类型


示例代码(不能直接运行)

char *addr;
int shmid;
...
if(addr = (char*)shmat(shmid, NULL, 0) == (cahr *)-1);
{
    perror("shmat");
    return -1;
}
fgets(addr , N , stdin);

shmdt:

不适用共享内存时候应该撤销映射关系


进程结束时候,操作系统会自动撤销


shmctl:

shmid 要操作的共享内存ID


cmd 要执行的操作 IPC_STAT(获取属性) IPC_SET(设置属性) IPC_RMID(删除ID)


struct shmid_ds *buf 表示共享内存属性


共享内存 – 注意事项

每块共享内存大小有限制

  ●  ipcs -l

  ●  cat /proc/sys/kernel/shmmax


共享内存删除的时间点

  ●  shmct(shmid, IC_RMID, NULL) 添加删除标记

  ●  nattach变成0时候真正删除


消息队列

●  消息队列是IPC对象的一种

●  消息队列由消息队列ID来唯一标识

●  消息队列就是一个消息列表,用户可以在消息队列中添加消息、读取消息

●  消息队列可以按照类型来发送、接收消息


使用步骤:

1.打开/创建见消息队列

2.向消息队列发送消息

3.从消息队列接收消息

4.控制消息队列

#include <sys/ipc.h
#include <sys/msg.h>
// 1. 打开/创建见消息队列
int msgget(key_t key, int msgflg);
// 2. 向消息队列发送消息
int msgsnd(int msgid, const void *msgp, size_t size, int msgflg);
// 3. 从消息队列接收消息
int msgrcv(int msgid, void *msgp, size_t size, long msgtype, int msgflg);
// 4. 控制消息队列
int msgctl(int msgid, int cmd, struct msqid_df *buf);

msgget:

key 和消息队列关联的key IPC_PRIVATE或ftok


msgflg 标志位IPC_CREATE | 0666


示例代码(不能直接运行)

int main()
{
    int msgid;
    key_t key;
    if((key = ftok(".", "q")) == -1)
    {
        perror("ftok");
        return -1;
    }
    if((msgid = msgget(key, IPC_CREATE|0666)) < 0)
    {
        perror("msgget");
        return -1;
    }
    return 0;
}

msgsnd:

msgid 消息队列ID


msgp 消息缓冲区地址


size 发送的消息长度(正文长度)


msgflg 标志位0或者IPC_NOWAIT


示例代码(不能直接运行)

typedef struct
{
  long mtype;
    char mtext[64];
}MSG
#define LEN (sizeof(MSG) - sizeof(long))
int main()
{   
  MSG buf;
  buf.mtype = 100;
    fgets(buf.mtext, 64, stdin);
    msgsnd(msgid, &buf, LEN, 0);
    ...
    return 0;
}    

消息格式

 ●  通信双方首先定义好统一的消息格式

 ●  用户根据应用需求定义结构体类型

 ●  首成员为long,代表消息类型(正整数)

 ●  其他成员为消息正文


msgrcv:

msgid 消息队列id


msgp 消息缓冲区地址


size 接收的消息长度(正文长度)


msgtype 指定接收的消息类型


msgflg 标志位0或者IPC_NOWAIT


示例代码(不能直接运行)

typedef struct
{
  long mtype;
    char mtext[64];
}MSG
#define LEN (sizeof(MSG) - sizeof(long))
int main()
{   
  MSG buf;
  buf.mtype = 100;
    fgets(buf.mtext, 64, stdin);
    if(msgrcv(msgid, &buf, LEN, 100,0) < 0)
    {
      perror("msgrcv");
        return -1;
    }
    ...
    return 0;
}    

msgctl:

msgid 消息队列ID


cmd 要执行的操作 IPC_STAT / IPC_SET / IPC_RMID


struct msqid_df*buf 表示消息队列属性地址


(消息被删除会立刻删除)


信号灯

信号灯又叫信号量,用于线程或者进程同步或者互斥的机制


信号灯类型


posix 无名信号灯


posix 有名信号灯


信号灯的含义 计数信号灯


system v 信号灯


  ●  是一个或者多个计数信号灯的集合

  ●  可以同时操作集合中的多个信号灯

  ●  申请多个资源时候避免死锁


使用步骤


1.打开/创建信号灯

2.信号灯初始化

3.PV操作

4.删除信号灯

// 1.打开/创建信号灯
int semget(key_t key, int nsems, int semflg);
// 2. 信号灯初始化
int semctl(int semid, int semnum. int cmd, ...)
// 3. PV操作
int semop(int semid, struct sembuf *sops, size_t nsops);
// 4. 删除信号灯  semctl

semget:


key 和信号灯关联的key IPC_PRIVATE或ftok


nsems 集合中包括的信号灯的个数


semflg 标志位IPC_CREATE | 0666 IPC_EXCL


semctl:


semid 信号灯的ID


semnum 要操作的信号灯的编号


cmd 执行的操作 SETVAL IPC_RMID


union semun 取决于cmd


semop:


semid 要操作的信号灯集


sops 描述对信号灯操作的结构体


nsops 要操作的信号灯个数

struct sembuf 
{
    short semnum;  // 信号灯编号
    short sem_op;  // -1  p操作  //  1  v操作
    short sem_flg; // 0  IPC_NOWAIT
}


目录
相关文章
|
1月前
|
消息中间件 并行计算 安全
进程、线程、协程
【10月更文挑战第16天】进程、线程和协程是计算机程序执行的三种基本形式。进程是操作系统资源分配和调度的基本单位,具有独立的内存空间,稳定性高但资源消耗大。线程是进程内的执行单元,共享内存,轻量级且并发性好,但同步复杂。协程是用户态的轻量级调度单位,适用于高并发和IO密集型任务,资源消耗最小,但不支持多核并行。
45 1
|
13天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
17天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
14天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
24 1
|
22天前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
36 7
|
22天前
|
消息中间件 存储 安全
|
20天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
36 2
|
21天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
28天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
Python
Python中的多线程与多进程
本文将探讨Python中多线程和多进程的基本概念、使用场景以及实现方式。通过对比分析,我们将了解何时使用多线程或多进程更为合适,并提供一些实用的代码示例来帮助读者更好地理解这两种并发编程技术。