进程与线程 -- 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
}


目录
相关文章
|
11天前
|
存储 Unix 程序员
进程与线程(二)线程相关
进程与线程(二)线程相关
21 1
|
6天前
|
Java 程序员 调度
【JAVA 并发秘籍】进程、线程、协程:揭秘并发编程的终极武器!
【8月更文挑战第25天】本文以问答形式深入探讨了并发编程中的核心概念——进程、线程与协程,并详细介绍了它们在Java中的应用。文章不仅解释了每个概念的基本原理及其差异,还提供了实用的示例代码,帮助读者理解如何在Java环境中实现这些并发机制。无论你是希望提高编程技能的专业开发者,还是准备技术面试的求职者,都能从本文获得有价值的见解。
24 1
|
11天前
|
存储 安全 Linux
进程与线程(一)进程相关
进程与线程(一)进程相关
20 1
|
4天前
|
数据采集 存储 安全
如何确保Python Queue的线程和进程安全性:使用锁的技巧
本文探讨了在Python爬虫技术中使用锁来保障Queue(队列)的线程和进程安全性。通过分析`queue.Queue`及`multiprocessing.Queue`的基本线程与进程安全特性,文章指出在特定场景下使用锁的重要性。文中还提供了一个综合示例,该示例利用亿牛云爬虫代理服务、多线程技术和锁机制,实现了高效且安全的网页数据采集流程。示例涵盖了代理IP、User-Agent和Cookie的设置,以及如何使用BeautifulSoup解析HTML内容并将其保存为文档。通过这种方式,不仅提高了数据采集效率,还有效避免了并发环境下的数据竞争问题。
如何确保Python Queue的线程和进程安全性:使用锁的技巧
|
9天前
|
存储 Java 编译器
进程和线程
进程和线程
92 25
|
2天前
|
调度
深入理解操作系统:进程与线程的管理
【8月更文挑战第29天】在数字世界的每一次点击和滑动背后,都隐藏着操作系统的精妙运作。本文将带你探索操作系统的核心概念之一——进程与线程的管理。我们将从基础定义出发,逐步深入到它们在内存中的表示、状态变迁以及它们之间错综复杂的关系。通过简洁明了的语言和直观的比喻,即便是没有计算机背景的读者也能轻松理解这一主题。准备好了吗?让我们一起揭开操作系统神秘的面纱,探索那些看似晦涩却无比精彩的知识吧!
|
4天前
|
调度 Python
深入理解操作系统:进程与线程的奥秘
【8月更文挑战第27天】本文将带你走进操作系统的核心,探索进程和线程这两个基本概念。我们将从它们的定义开始,逐步深入到它们之间的联系和区别,以及在操作系统中的作用。通过本文,你将了解到进程和线程不仅仅是编程中的两个术语,它们是操作系统管理资源、实现并发和并行的关键。最后,我们还将通过一个代码示例,展示如何在Python中创建和管理线程。
|
3天前
|
算法 调度 开发者
深入理解操作系统:进程与线程管理
【8月更文挑战第28天】在数字世界的心脏跳动着的是操作系统,它是计算机硬件与软件之间的桥梁。本文将带你探索操作系统的核心概念——进程与线程,揭示它们如何协同工作以支持多任务处理和并发执行。通过实际代码示例,我们将深入了解这些抽象概念是如何在真实系统中实现的。无论你是编程新手还是资深开发者,这篇文章都将为你提供新的视角,让你对操作系统有更深刻的认识。
|
7天前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
16天前
|
Java 调度
基于C++11的线程池
基于C++11的线程池

热门文章

最新文章

相关实验场景

更多
下一篇
云函数