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


目录
打赏
0
0
0
0
10
分享
相关文章
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
125 67
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
70 0
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
80 16
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
67 6
如何区分进程、线程和协程?看这篇就够了!
本课程主要探讨操作系统中的进程、线程和协程的区别。进程是资源分配的基本单位,具有独立性和隔离性;线程是CPU调度的基本单位,轻量且共享资源,适合并发执行;协程更轻量,由程序自身调度,适合I/O密集型任务。通过学习这些概念,可以更好地理解和应用它们,以实现最优的性能和资源利用。
115 11
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
141 6
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!

热门文章

最新文章

相关实验场景

更多
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等