#IO进程 笔记二

进程 Process

1. 概念

程序:编译好的可执行文件
存放在磁盘上的指令和数据的有序集合(文件)
程序是静态的,没有任何执行的概念
进程:一个独立的可调度任务
执行一个程序所分配资源的总称
进程是程序执行的一次过程
进程是动态的,包括创建、调度、执行和消亡

2. 特点

(1) 系统会为每一个分配0-4g虚拟空间,0-3g(用户空间)是每个进程所独有的,3-4g(内核空间)是所有进程共有的。


(2) CPU调度进程时会给进程分配时间片(几毫秒~十几毫秒),当时间片用完后,cpu再进行其他进程的调度,实现进程的轮转,从而实现多任务的操作。(没有外界干预的情况下怎么调度进程是CPU随机分配的 )

进程控制块task_struct
● 进程控制块pcb: 包含描述进程的相关信息
● 进程标识PID:唯一的标识一个进程
主要进程标识:
进程号(PID: Process Identity Number)
父进程号:(Parent Process ID: PPID)
● 进程用户
● 进程状态、优先级
● 文件描述符(记录当前进程打开的文件)

3. 进程段

Linux中的进程大致包含三个段:
数据段:存放的是全局变量、常数以及动态数据分配的数据空间(如malloc函数取得的空间)等。
正文段:存放的是程序中的代码
堆栈段:存放的是函数的返回地址、函数的参数以及程序中的局部变量 (类比内存的栈区)

5. 进程状态

D uninterruptible sleep (usually IO) 不可中断的睡眠态
R running or runnable (on run queue) 运行态
S interruptible sleep (waiting for an event to complete) 可中断的睡眠态
T stopped by job control signal 暂停态
t stopped by debugger during the tracing 因为调试而暂停
X dead (should never be seen) 死亡态
Z defunct ("zombie") process, terminated but not reaped by its parent 僵尸态
< high-priority (not nice to other users) 高优先级
N low-priority (nice to other users) 低优先级
L has pages locked into memory (for real-time and custom IO) 锁在内存中
s is a session leader 会话组组长
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)多线程
+ is in the foreground process group 前台进程
没有+时,默认是后台进程
I (大写i)空闲状态进程

6. 进程状态切换图

image.png


进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态。

7. 进行相关命令

ps 查看系统中的进程 -ef -aux
top 动态查看系统中的进程
nice 按用户指定的优先级运行进程
renice 修改进程的优先级
kill 给进程发送信号
bg 将进程切换到后台执行
fg 将进程切换到前台执行
jobs 查看当前终端的进程
补充:优先级调度(面试可能遇见)
根据进程的优先级进行调度,优先级高的进程先执行。
两种类型:
1. 非剥夺式(非抢占式)优先级调度算法。当一个进程正在处理上运行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在进行的进程继续运行,直到由于其自身原因而主动让出处理机(任务完成或等待事件),才把处理机分配给更为重要或紧迫的进程。
2. 剥夺式(抢占式)优先级调度算法。当一个进程正在处理机上运行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给更重要或紧迫的进程。

面试题:
1. 下列关于轮询任务调度和可抢占式调度区别描述错误的是?D
A. 抢占式调度实现相对较复杂且可能出现低优先级的任务长期得不到调度
B. 轮询调度不利于后面的请求及时得到响应
C. 抢占式调度有利于后面的高优先级的任务也能及时得到响应
D. 抢占式调度优点是其简洁性,它无需记录当前所有连接的状态
解释: 抢占式调度相对于轮询调度来说实现相对复杂,需要记录和管理任务的优先级、状态等信息。
2. 会导致进程从执行态变为就绪态的事件是( D)。(大明科技)
A. 执行P(wait)操作
B. 申请内存失败
C. 启动I/O设备
D. 被高优先级进程抢占
3. 分配到必要的资源并获得处理机时的进程状态是( B)。(大明科技)
A. 就绪状态
B. 执行状态
C. 阻塞状态

8. 进程函数接口

8.1 创建进程 fork()

pid_t fork(void);
功能:创建子进程
返回值:
    成功:在父进程中:返回子进程的进程号 >0
         在子进程中:返回值为0
    失败:-1并设置errno
#include<stdio.h>
#include<unistd.h>

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid=fork();
    if(pid<0)
    {
        perror("fork err");
        return -1;
    }
    else if(pid==0)  //子
    {
        printf("child\n");
        while(1); //让子进程不要结束
    }
    else //父
    {
        printf("parent\n");
        while(1); //让父进程不要结束
    }
     
    return 0;
}

特点:
1)子进程几乎拷贝了父进程的全部内容。包括代码、数据、系统数据段中的pc值、栈中的数据、父进程中打开的文件等;但它们的PID、PPID是不同的。

image.png


2)父子进程有独立的地址空间,互不影响;当在相应的进程中改变全局变量、静态变量,都互不影响。

image.png


3)若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程。

image.png

image.png


4)若子进程先结束,父进程如果没有及时回收资源,子进程变成僵尸进程(要避免僵尸进程产生)

image.png

image.png


实现ls -l

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
 #include <time.h>

int main(int argc, char const *argv[])
{
    struct stat st;
    if (stat(argv[1], &st) < 0)
    {
        perror("stat err");
        return -1;
    }

    //判断文件类型
    switch (st.st_mode & S_IFMT)
    {
    case S_IFREG:
        printf("-");
        break;
    case S_IFDIR:
        printf("d");
        break;
    case S_IFCHR:
        printf("c");
        break;
    case S_IFIFO:
        printf("f");
        break;
    case S_IFLNK:
        printf("l");
        break;
    case S_IFBLK:
        printf("b");
        break;
    case S_IFSOCK:
        printf("s");
        break;
    default:
        printf("mode err\n");
        break;
    }

    //判断文件权限
    //个人权限
    if (st.st_mode & S_IRUSR)
        printf("r");
    else
        printf("-");
    if (st.st_mode & S_IWUSR)
        printf("w");
    else
        printf("-");
    if ((st.st_mode & S_IXUSR))
        printf("x");
    else
        printf("-");
    
    //小组权限
    if (st.st_mode & S_IRGRP)
        printf("r");
    else
        printf("-");
    if (st.st_mode & S_IWGRP)
        printf("w");
    else
        printf("-");
    if ((st.st_mode & S_IXGRP))
        printf("x");
    else
        printf("-");
    //其他人权限
    if (st.st_mode & S_IROTH)
        printf("r");
    else
        printf("-");
    if (st.st_mode & S_IWOTH)
        printf("w");
    else
        printf("-");
    if ((st.st_mode & S_IXOTH))
        printf("x");
    else
        printf("-");

    //链接数
    printf(" %d",st.st_nlink);
    //用户名 用getpwuid函数
    printf(" %s", getpwuid(st.st_uid)->pw_name);
    //组名 用getgrgid函数
    printf(" %s",getgrgid(st.st_gid)->gr_name);
    //文件大小
    printf(" %ld",st.st_size);
    //最后修改时间
    printf(" %.12s",ctime(&st.st_mtime)+4); //+4:偏移4地址跳过前4位,.12:只打印12个字符,默认右对齐
    //名字
    printf(" %s\n",argv[1]);
    return 0;
}


复习函数指针, 可以看以下例子
数据类型 (* 指针名)(参数列表);

#include <stdio.h>
#include <stdlib.h>
int test(int (*p)(int, int), int a, int b) //p=fun ,a=3,b=4
{
    return p(a,b); //fun(3,4);
}
int fun(int n, int m) //n=3,m=4
{
    return n * m;
}

int main(int argc, char const *argv[])
{
      printf("%d\n", test(fun, 3, 4));
      return 0;
}


8.2 回收资源

pid_t wait(int *status);
功能:回收子进程资源(阻塞)
参数:status:子进程退出状态,不接受子进程状态设为NULL
返回值:成功:回收的子进程的进程号
              失败:-1

pid_t waitpid(pid_t pid, int *status, int options);
功能:回收子进程资源
参数:
    pid:>0     指定子进程进程号
         =-1   任意子进程
         =0    等待其组ID等于调用进程的组ID的任一子进程
         <-1   等待其组ID等于pid的绝对值的任一子进程
    status:子进程退出状态
    options:0:阻塞 WNOHANG:非阻塞
返回值:正常:结束的子进程的进程号
      当使用选项WNOHANG且没有子进程结束时:0
      出错:-1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) //子
    {
        sleep(2);
        printf("child\n");
    }
    else //父
    {
        printf("parent\n");
        // wait(NULL);
        // waitpid(-1,NULL,0); //0:阻塞,此时和wait(NULL)一样
        //waitpid(-1, NULL, WNOHANG); //WNOHANG:非阻塞,此时可能回收不到子进程资源

        //所以可以循环调用waitpid保证一定回收到子进程资源
        while (1)
        {
            if (waitpid(-1, NULL, WNOHANG) > 0)
                break;
        }

        while (1)
            ; //让父进程不要结束
    }

    return 0;
}


8.3 结束进程

void exit(int status);
功能:结束进程,刷新缓存

void _exit(int status);
功能:结束进程,不刷新缓存
参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。
    通常0表示正常结束;
其他的数值表示出现了错误,进程非正常结束


例如:
exit结束进程,return结束当前函数。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int fun()
{
    printf("in fun\n");
    //return 0;
    exit(0);
}

int main(int argc, char const *argv[])
{
    printf("hello\n");
    fun();
    printf("in main\n");
    while(1);

    return 0;
}


8.4 获取进程号

pid_t getpid(void);
功能:获取当前进程的进程号

pid_t getppid(void);
功能:获取当前进程的父进程号

例如:父子进程中分别打印这两个进程的进程号

image.png

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        printf("child:%d %d\n", getpid(), getppid());
        while (1)
            ;
    }
    else
    {
        printf("parent:%d %d\n", pid, getpid());
        while (1)
            ;
    }

    return 0;
}


9. exec函数族(了解)

在一个进程中执行另一个进程:
system("clear");
system("ls -l");

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{

    printf("hello\n");
    //system("ls -l");  //原来进程不会被替换,执行完召唤的进程之后还会继续进行
    execl("/bin/ls","ls","-l",NULL); //该进程被召唤进程完全替换,所以后面内容不打印了
    printf("world\n");

    return 0;
}


原型:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

10. 守护进程 Daemon

Linux以会话(session)、进程组的方式管理进程,每个进程属于一个进程组,也就是多个进程组成一个进程组。会话是一个或多个进程组的集合,通常用户打开一个终端时,系统会创建一个会话。所有通过该终端运行的进程都属于这个会话。终端关闭时,所有相关进程会被结束。
但是守护进程却能突破这种限制,不受终端关闭的影响。

10.1 守护进程特点

守护进程是后台进程;
生命周期比较长,从系统启动时开启,系统关闭时结束;
它是脱离控制终端且周期执行的进程。

10.2 创建步骤

(1) 创建子进程,父进程退出
让子进程变成孤儿进程,成为后台进程;fork()
(2) 在子进程中创建新会话
让子进程成为会话组组长并且脱离终端:为了让子进程完全脱离终端;setsid()
(3) 改变进程运行路径为根目录
原因: 进程运行的路径不能被删除或卸载;chdir("/")
函数说明:chdir() 将进程当前的工作目录改变成以参数路径所指的目录
(4) 重设文件权限掩码
目的:增大进程创建文件时权限,提高灵活性;umask(0)
子进程继承了父进程的文件权限掩码,给该子进程使用文件带来一定的影响,因此把文件 权限掩码设置为0,可以增强该守护进程的灵活性。
(5) 关闭文件描述符
原因:子进程继承了父进程的一些已经打开了的文件,这些被打开的文件可能永远不会被 守护进程访问,但它们一样占用系统资源,而且还可能导致所在的文件系统无法被卸载。
将不需要的文件关闭:close()

image.png


练习:创建一个守护进程,循环间隔1s向文件中写入一串字符“hello”

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    int fd;
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork er r");
        return -1;
    }
    else if (pid == 0)
    {
        setsid();
        chdir("/");
        umask(0);
        for (int i = 0; i < 3; i++)
            close(i);
        fd = open("/tmp/test.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);

        while (1)
        {
            sleep(1);
            write(fd, "hello", 5);
        }
    }
    else
    {
        exit(0);
    }

    return 0;
}


总结守护进程:
● 守护进程是一个生存周期较长的进程,通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件
● 大多数服务都是通过守护进程实现的
● 关闭终端,相应的进程都会被关闭,而守护进程却能够突破这种限制

线程 Thread

1.线程

1. 概念

线程是一个轻量级的进程,为了提高系统的性能引入的。
线程和进程都参与统一调度。
在同一个进程中可以创建的多个线程, 共享进程资源,但是需要考虑临界资源的问题。
(Linux里同样用task_struct来描述一个线程)

2. 进程和线程的区别

相同点:都为操作系统提供了并发执行的能力
不同点:
临界资源访问的问题;进程通信比较复杂,需要借助进程间的通信机制(借助3g-4g内核空间)
地址空间方面:同一进程内的多线程共享该进程资源,进程的地址空间相互独立
调度和资源:线程是系统调度的最小单位,进程资源分配的最小单位。
通信方面:线程通信相对简单,只需要通过全局变量可以实现,但是需要考虑
安全性方面:线程安全性差一些,当进程结束时会导致所有线程退出;进程相对安全

面试题:程序什么时候该使用线程?什么时候用进程?(深圳棱镜空间智能科技有限公司)(北京明朝万达)
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。

3. 线程资源

共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID
私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性

2. 函数接口

2.1 创建线程: pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine) (void *), void *arg);
功能:创建线程
参数:       thread:线程标识
            attr:线程属性, NULL:代表设置默认属性
            start_routine:函数名:代表线程函数(自己写的)
            arg:用来给前面函数传参
返回值:成功:0
      失败:错误码
      
编译的时候需要加 -pthread 链接动态库

函数指针: 数据类型(* 指针名)(参数列表);

#include <pthread.h>
#include<stdio.h>

//从线程
void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    while(1);

    return NULL;
}

//主线程
int main(int argc, char const *argv[])
{
    pthread_t tid;
    if(pthread_create(&tid,NULL,handler_thread,NULL) !=0)
    {
        perror("phtread create err");
        return -1;
    }
    printf("in main\n");
    while(1);

    return 0;
}

image.png


2.2 退出线程:pthread_exit()

void  pthread_exit(void *value_ptr) 
    功能:用于退出线程的执行
参数:value_ptr:线程退出时返回的值

image.png


可以看出从线程退出了,就剩下主线程一个线程,不是多线程了。

2.3 回收线程资源

int  pthread_join(pthread_t thread,  void **value_ptr) 
    功能:用于等待一个指定的线程结束从而回收该线程资源,阻塞函数
参数:thread:创建的线程对象,线程ID
     value_ptr:指针*value_ptr指向线程返回的参数, 一般为NULL
返回值:成功 : 0
    失败:errno

int pthread_detach(pthread_t thread);
功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数
参数:thread:线程ID
非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach(),pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源
#include <pthread.h>
#include<stdio.h>

//从线程
void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    pthread_exit(NULL); //退出当前线程

    return NULL;
}

//主线程
int main(int argc, char const *argv[])
{
    pthread_t tid;
    if(pthread_create(&tid,NULL,handler_thread,NULL) !=0)
    {
        perror("phtread create err");
        return -1;
    }
    printf("in main\n");

    //回收线程资源函数
    //pthread_join(tid,NULL); //阻塞函数,等待指定线程退出回收该线程资源
    pthread_detach(tid);   //非阻塞函数,让指定线程结束时,自己回收自己的资源

    while(1);
    return 0;
}

练习: 通过父子进程完成对文件的拷贝(cp)
1. 通过父子进程完成对文件的拷贝(cp),父进程从文件开始到文件的一半开始拷贝,子进程从文件的一半到文件末尾。要求:文件IO cp src dest
1) 文件长度获取?lseek
2) 子进程定位到文件一半的位置 lseek
3) 父进程怎么能准确读到文件一半的位置
4) fork之前打开文件,父子进程中读写文件时,位置指针是同一个

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    int fd_src, fd_dest;
    char buf[32] = "";
    pid_t pid;
    ssize_t s;

    if (argc != 3)
    {
        printf("format: %s <srcfile> <destfile>\n", argv[0]);
        return -1;
    }

    fd_src = open(argv[1], O_RDONLY);
    if (fd_src < 0)
    {
        perror("src err");
        return -1;
    }

    fd_dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd_dest < 0)
    {
        perror("dest err");
        return -1;
    }

    //计算一半长度
    off_t len = lseek(fd_src, 0, SEEK_END) / 2;
    pid = fork();

    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) //从一半到末尾
    {
        //定位操作,源文件和目标文件位置指针定位到一半
        lseek(fd_src, len, 0);
        lseek(fd_dest, len, 0);

        //循环读源文件,只要读到就写到目标文件
        while (1)
        {
            s = read(fd_src, buf, 32);
            if (s == 0)
                break;
            write(fd_dest, buf, s);
            sleep(1);
        }
    }
    else //从开头到一半
    {
        wait(NULL); //先让子读写完成,再让父读写,避免父子进程用一个文件指针读写造成混乱

        //定位操作,源文件和目标文件位置指针定位到开头
        lseek(fd_src, 0, 0);
        lseek(fd_dest, 0, 0);

        //利用读了多少减少len,len为0就停止,len表示剩下要读的字符个数
        while (len > 0)
        {
            if (len > 32)
                s = read(fd_src, buf, 32);
            else
                s = read(fd_src, buf, len);
            write(fd_dest, buf, s);
            len = len - s;  //len存放剩余需要读的个数
            sleep(1);
        }
    }
    close(fd_dest);
    close(fd_src);
    return 0;
}


作业:
1. 吸收今天所学内容,整理笔记,完善代码,代码敲两遍。
2. 实现以下练习,尽力实现
输入输出,quit结束
通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。
1) 全局变量进行通信
2) 加上标志位(flag),实现主线程输入一次,线程函数打印一次, int flag = 0;

#include <stdio.h>
#include <pthread.h>
#include <string.h>

char s[32];
int flag = 0;

void *handler_thread(void *arg)
{
    while (1)
    {
        if (flag == 1)
        {
            if (strcmp(s, "quit") == 0)
                break;
            printf("%s\n", s);
            flag = 0;
        }
    }
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("err");
        return -1;
    }

    while (1)
    {
        // if (flag == 0) 
        // {
            scanf("%s", s);
            flag = 1;
            if (strcmp(s, "quit") == 0)
                break;
        // }
    }

    return 0;
}


3. 同步

3.1 概念

同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情
(异步:异步则反之,并非一定需要一件事做完再做另一件事。)

3.2 同步机制

通过信号量实现线程间同步。
信号量:通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待.
信号量代表某一类资源,其值表示系统中该资源的数量:
信号量的值>0,表示有资源可以用, 可以申请到资源,
信号量的值<=0, 表示没有资源可以通用, 无法申请到资源, 阻塞.
信号量还是一个受保护的变量,只能通过三种操作来访问:初始化、P操作(申请资源)、V操作(释放资源)
sem_init: 信号量初始化
sem_wait: 申请资源,P操作,如果没有资源可用,阻塞,直到有资源可用资源-1
sem_post: 释放资源,V操作,非阻塞,资源+1

3.3 函数接口

int  sem_init(sem_t *sem,  int pshared,  unsigned int value)  
    功能:初始化信号量   
参数:sem:初始化的信号量对象
    pshared:信号量共享的范围(0: 线程间使用   非0:1进程间使用)
    value:信号量初值
返回值:成功 0
    失败 -1

    int  sem_wait(sem_t *sem)  
    功能:申请资源  P操作 
参数:sem:信号量对象
返回值:成功 0
    失败 -1
    注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞

int  sem_post(sem_t *sem)   
    功能:释放资源  V操作      
参数:sem:信号量对象
返回值:成功 0
    失败 -1
    注:释放一次信号量的值加1,函数不阻塞
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>

char s[32];
sem_t sem;
sem_t sem2;

void *handler_thread(void *arg)
{
    while (1)
    {
       sem_wait(&sem);    //等待输入完成

        if (strcmp(s, "quit") == 0)
            break;
        printf("%s\n", s); //输出完成通知输入
        sem_post(&sem2);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("err");
        return -1;
    }

    //初始化信号量
    sem_init(&sem, 0, 0);
    sem_init(&sem2, 0, 1);

    while (1)
    {
        sem_wait(&sem2); //等待上一次输出完成
        scanf("%s", s);
        
        sem_post(&sem); //输入完成通知输出

        if (strcmp(s, "quit") == 0)
            break;
    }

    pthread_join(tid, NULL);

    return 0;
}

4. 互斥

4.1 概念

互斥:多个线程在访问临界资源的时候,同一时间只能一个线程访问
临界资源:一次仅允许一个线程所使用的资源
临界区: 指的是一个共享资源的程序片段
互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock

4.2 函数接口

int  pthread_mutex_init(pthread_mutex_t  *mutex, pthread_mutexattr_t *attr)  
功能:初始化互斥锁  
参数:mutex:互斥锁
    attr:  互斥锁属性  //  NULL表示缺省属性
返回值:成功 0
      失败 -1
      
int  pthread_mutex_lock(pthread_mutex_t *mutex)   
功能:申请互斥锁     
参数:mutex:互斥锁
返回值:成功 0
      失败 -1

注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

int  pthread_mutex_unlock(pthread_mutex_t *mutex)   
功能:释放互斥锁     
参数:mutex:互斥锁
返回值:成功 0
      失败 -1

int  pthread_mutex_destroy(pthread_mutex_t  *mutex)  
功能:销毁互斥锁     
参数:mutex:互斥锁
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;

void *handler_swap(void *arg)
{
    sleep(1); //保证主线程先初始化完互斥锁
    int t;
    while (1)
    {
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 5; i++)
        {
            t = a[i];
            a[i] = a[9 - i];
            a[9 - i] = t;
        }
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

void *handler_print(void *arg)
{
    sleep(1); //保证主线程先初始化完互斥锁
    while (1)
    {
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 10; i++)
            printf("%d ", a[i]);
        printf("\n");
        pthread_mutex_unlock(&lock);
        sleep(1); //锁里面减少耗时大的操作
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, handler_swap, NULL) != 0)
    {
        perror("err1");
        return -1;
    }

    if (pthread_create(&tid2, NULL, handler_print, NULL) != 0)
    {
        perror("err2");
        return -1;
    }

    //初始化
    if (pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("init err");
        return -1;
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

补充:死锁

是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

5. 条件变量

条件变量用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。
一般和互斥锁搭配使用,实现同步机制:
pthread_cond_init(&cond,NULL); //初始化条件变量
使用前需要上锁:
pthread_mutex_lock(&lock); //上锁
判断条件
pthread_cond_wait(&cond,&lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁
pthread_cond_signal(&cond); //产生条件,不阻塞
pthread_cond_destroy(&cond); //销毁条件变量
注意: 必须保证让pthread_cond_wait先执行,pthread_cond_signal再产生条件

image.png


为什么pthread_cond_wait前要加互斥锁:防止调用pthread_cond_wait之前其他线程改变条件,从而发生错乱。例如线程 A 判断条件不满足之后,调用 pthread_cond_wait 之前可能因为线程 B 更改了条件,使得条件满足然后调用pthread_cond_signa。但此时线程 A 还没有调用 pthread_cond_wait。等到线程 A 又启动调用 pthread_cond_wait 后虽然条件满足,但却收不到 pthread_cond_signal 的唤醒,就一直阻塞下去。
例如: 赚钱花钱

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int money = 0; //总钱数
pthread_cond_t cond;
pthread_mutex_t lock;

//花钱
void *spend_money(void *arg)
{

    while (1)
    {
        pthread_mutex_lock(&lock);

        if (money < 100)
            pthread_cond_wait(&cond, &lock);

        while (money >= 100)
        {
            money -= 100;
            printf("money:%d trillion\n", money);
        }

        pthread_mutex_unlock(&lock);
    }

    return NULL;
}

//赚钱
int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, spend_money, NULL) != 0)
    {
        perror("err");
        return -1;
    }
    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&cond, NULL);

    //赚钱
    while (1)
    {
        sleep(2); //为了保证让pthread_cond_wait先执行
        pthread_mutex_lock(&lock);

        scanf("%d", &money);
        if (money >= 100)
            pthread_cond_signal(&cond);

        pthread_mutex_unlock(&lock);
    }

    return 0;
}


练习:请在linux 利用c语言编程实现两个线程按照顺序依次输出”ABABABAB......" (信雅达)
例如a线程输出”A”之后b线程输出”B”,然后a线程输出“A”,再b线程输出”B”,之后往复循环。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int money = 0; //总钱数
pthread_cond_t cond;
pthread_mutex_t lock;

//花钱
void *spend_money(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&cond, &lock);
        printf("B\n");
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

//赚钱
int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, spend_money, NULL) != 0)
    {
        perror("err");
        return -1;
    }
    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&cond, NULL);

    //赚钱
    while (1)
    {
        sleep(2); //为了保证让pthread_cond_wait先执行
        pthread_mutex_lock(&lock);
        printf("A\n");
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&lock);
    }
    return 0;
}


进程间通信IPC

InterProcess Communication
进程通信方式
(1) 早期进程间通信方式:
无名管道(pipe)、有名管道(fifo)、信号(signal)
(2) system V IPC
共享内存(shared memory)、消息队列(message queue)、信号灯集(semaphore set)
(3) BSD:
套接字(socket)

1. 无名管道

image.png


1.1 特点

(1) 只能用于具有亲缘关系的进程之间的通信
(2) 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
(3) 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
(4) 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

image.png


1.2 函数接口

int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
        失败 -1

image.png

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[65536] = "";
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    printf("%d %d\n", fd[0], fd[1]);

    //读写
    //write(fd[1], "hello", 5);
    // read(fd[0], buf, 5);
    // printf("%s\n",buf);

    //结构类似队列,先进先出
    //1.当管道中无数据,读阻塞
    // read(fd[0], buf, 32);
    // printf("%s\n", buf);

    //但是关闭写端就不一样了
    //当管道中有数据的时候关闭写端可以读出数据。
    //但是当管道中无数据,关闭写写端,读操作会立即返回。
    // close(fd[1]);
    // read(fd[0], buf, 32);
    // printf("%s\n", buf);

    //2.当管道中写满数据的时候,写操作会阻塞,管道大小为64K
    // write(fd[1], buf, 65536);
    // printf("full\n");
    // //write(fd[1], "h", 1);
    // //写满一次之后,当管道中至少再有4K空间时(也就是再读4K)才能继续写,否则阻塞
    // read(fd[0],buf,4096);//如果改成4065就阻塞,因为不够4K空间
    // write(fd[1],"a",1);
    // printf("after\n");

    //3.当关闭读端的时候,写就无意义,管道破裂,进程会收到内核发送的SIGPIPE信号
    close(fd[0]);
    write(fd[1],"a",1);
    printf("read close\n");
    //gdb调试可以看到管道破裂信号

    return 0;
}

image.png


1.3 注意事项

(1) 当管道中无数据时,读操作会阻塞。
管道中有数据,将写端关闭,可以将数据读出。
管道中无数据,将写端关闭,读操作会立即返回。
(2) 管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续
(3) 只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。
练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
提示:不需要加同步机制, 因为pipe无数据时读会阻塞。
考虑:创建管道是在fork之前还是之后? 先pipe再fork才能获取到一个管道
思路

pipe()
pid=fork()
if(pid==0)
{
  while(1)
  {
    //读管道
    //判断quit,break
    printf()
}
else
{
    while(1)
    {
    scanf()
    //写管道
    //判断quit,break
      }
  //回收子进程
}


代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    pid_t pid;
    char buf[32] = "";
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }

    if ((pid = fork()) < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        //循环打印
        while (1)
        {
            read(fd[0], buf, 32);
            if (strcmp(buf, "quit") == 0)
                break;
            printf("%s\n", buf);
        }
    }
    else
    {
        //循环输入
        while (1)
        {
            scanf("%s", buf);
            //write(fd[1], buf, 32);
            write(fd[1], buf, strlen(buf)+1); //+1是为了最后要写入\0
            if (strcmp(buf, "quit") == 0)
                break;
        }
        wait(NULL);
    }

    return 0;
}


补充: typedef 给数据类型重命名

#include <stdio.h>

//给普通数据类型int重命名
typedef int size4;        

//给指针类型int* 重命名
typedef int *int_p;       

//给数组类型int [10]重命名
typedef int intArr10[10]; 

//给函数指针void (*)()重命名
typedef void (*fun_p)(); 

void fun()
{
    printf("fun\n");
}

int main(int argc, char const *argv[])
{
    size4 a = 10;             //相当于int a=10;
    int_p p = &a;             //相当于int* p=&a;
    intArr10 arr = {1, 2, 3}; //相当于int arr[10]={1,2,3};
    fun_p fp = fun;           //相当于 void (*fp)()=fun;
    printf("%d\n", *p);
    printf("%d\n", arr[0]);
    fp();

    return 0;
}


总而言之,定义变量的变量名写在哪里,用typedef给数据类型重命名的新名字就写在哪里。然后使用新名字定义变量的格式直接就可以为:新名字 变量名;

2. 有名管道

2.1 特点

1) 有名管道可以使互不相关的两个进程互相通信。
2) 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
3) 进程通过文件IO来操作有名管道
4) 有名管道遵循先进先出规则
5) 不支持如lseek() 操作
操作方式:先创建有名管道,然后open、read、write

2.2 函数接口

int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
       mode:权限
返回值:成功:0
       失败:-1,并设置errno号
注意对错误的处理方式:
如果错误
是file exist时,注意加判断,如:if(errno == EEXIST)

image.png


2.3 注意事项

1) 只写方式打开阻塞,一直到另一个进程把读打开
2) 只读方式打开阻塞,一直到另一个进程把写打开
3) 可读可写,如果管道中没有数据,读阻塞

#Cinclude <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[32] = "";
    int fd;
    if (mkfifo("fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf("file exist\n");
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    printf("mkfifo success\n");

    //打开文件
    fd = open("fifo", O_RDWR);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }

    //读写操作
    write(fd, "hello", 5);
    read(fd, buf, 32);
    printf("buf:%s\n", buf);
    return 0;
}


练习:通过两个进程实现cp功能。
./input srcfile
./output destfile
思路:
input.c 读源文件,写进管道
//创建有名管道
//打开管道文件,打开源文件
//循环读源文件,写进管道文件

#include <stCdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    char buf[32] = "";
    int fd_fifo, fd_file;
    ssize_t s;
    if (mkfifo("fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf("file exist\n");
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    printf("mkfifo success\n");

    //打开管道文件,打开源文件
    fd_fifo = open("fifo", O_WRONLY);
    if (fd_fifo < 0)
    {
        perror("open fifo err");
        return -1;
    }

    fd_file = open(argv[1], O_RDONLY);
    if (fd_file < 0)
    {
        perror("open file err");
        return -1;
    }

    //读源文件,写入管道文件
    while (1)
    {
        s=read(fd_file,buf,32);//从源文件中读出数据放入buf
        if(s==0)
            break;
        write(fd_fifo,buf,s);//把buf中的数据写入管道
       
    }
    
    close(fd_fifo);
    close(fd_file);

    return 0;
}


output.c 循环读管道,写进目标文件
//创建有名管道
//打开管道文件,打开目标文件
//循环读管道文件,写进目标文件

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    char buf[32] = "";
    int fd_fifo, fd_file;
    ssize_t s;
    if (mkfifo("fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf("file exist\n");
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    printf("mkfifo success\n");

    //打开管道文件,打目标文件
    fd_fifo = open("fifo", O_RDONLY);
    if (fd_fifo < 0)
    {
        perror("open fifo err");
        return -1;
    }

    fd_file = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd_file < 0)
    {
        perror("open file err");
        return -1;
    }

    //读管道文件,写目标文件
    while (1)
    {
        s = read(fd_fifo, buf, 32); //把管道中内容读到buf
        if (s == 0)
            break;
        write(fd_file, buf, s); //把buf中内容写到目标文件
    }

    close(fd_fifo);
    close(fd_file);

    return 0;
}


有名管道和无名管道的区别

无名管道

有名管道

使用场景

具有亲缘关系的进程间

可以不相关两个进程间

特点

半双工通信方式
固定读端fd[0]和写端fd[1]
可看作特殊文件
用文件IO进程操作

在文件系统中会存在管道文件,数据放在内核空间
通过文件IO进程操作
不支持lseek操作,遵循先进先出

函数

pipe()
直接read、write

mkfifo()
先打开open、再read/write

读写特性

当管道中无数据读阻塞
当管道写满时写阻塞
当读端关闭,往管道中写会导致管道破裂

只写方式打开阻塞,直到另一个进程把读打开
只读方式打开阻塞,直到另一个进程把写打开
可读可写打开,如果管道中无数据,读阻塞

3. 信号

kill -l: 显示系统中的信号
kill -num PID: 给某个进程发送信号

3.1 概念

1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号的生命周期:

image.png


3.2信号的响应方式

1)忽略信号: 对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作

3.3 信号种类

SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

3.4 函数接口

3.4.1 信号发送和挂起
#include <signal.h>
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
   sig:要发送的信号
返回值:成功 0     
       失败 -1

int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0   
       失败 -1
相当于: kill(getpid(),sig);

int pause(void);
功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    //kill(getpid(),SIGKILL); //给当前进程发送了SIGKILL信号
    raise(SIGKILL);           //相当于kill(getpid(),SIGKILL);
    //while(1);
    pause();        //将进程挂起,所用和死循环类似,但是不占用CPU。

    return 0;
}
3.4.2 定时器
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。

系统默认对SIGALRM(闹钟到点后内核发送的信号)信号的响应: 如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出进程。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf("%d\n",alarm(10)); //设定闹钟给你10秒后发送闹钟信号,第一次调用返回0
    sleep(2);                 //睡眠两秒,此时闹钟还剩8秒
    printf("%d\n",alarm(3));  //重新设置闹钟3秒后发送闹钟信号,上个闹钟还剩8秒
    pause();                  //将进程挂起,否则进程直接结束了也等不到闹钟信号
    printf("hello\n");        //进程收到闹钟信号直接结束进程了这句也不会打印因为执行不到这里

    return 0;
}

image.png


3.4.3 信号处理函数signal()
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
      handler:信号处理方式
          SIG_IGN:忽略信号  (忽略 ignore)
          SIG_DFL:执行默认操作 (默认 default)
          handler:捕捉信号 (handler为函数名,可以自定义)
     void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
      失败:-1

例子:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
    printf("clrl C:%d\n",sig);
}

int main(int argc, char const *argv[])
{
    //signal(SIGINT,SIG_DFL); //对SIGINT信号设置处理方式为默认处理方式
    //signal(SIGINT,SIG_IGN); //对SIGINT信号设置处理方式为忽略
    signal(SIGINT,handler); //对SIGINT信号设置处理方式为捕捉模式,也就是自定义处理方式
    //while(1); //让进程不结束
    pause();    //当收到被捕获模式处理的信号的时候,就结束挂起,打印后面语句
    printf("after pause\n"); 
    return 0;
}


例子:

image.png


练习:
用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:司机(父进程)、售票员(子进程)
售票员:捕捉:SIGINT、SIGQUIT、SIGUSR1
忽略:SIGTSTP
司机:捕捉:SIGUSR1、SIGUSR2、SIGTSTP
忽略:SIGINT、SIGQUIT

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t pid;

void saler(int sig)
{
    if (sig == SIGINT)
        kill(getppid(), SIGUSR1);
    else if (sig == SIGQUIT)
        kill(getppid(), SIGUSR2);
    else if (sig == SIGUSR1)
    {
        printf("slaser: pls get off the bus\n");
        exit(0);
    }
}

void driver(int sig)
{
    if (sig == SIGUSR1)
        printf("driver: lets gogogo!!!\n");
    else if (sig == SIGUSR2)
        printf("driver: stop the bus!!!\n");
    else if (sig == SIGTSTP)
    {
        kill(pid, SIGUSR1); //pid在父进程中是子进程的进程号
        wait(NULL); //司机等待售票员下车以后再下车
        exit(0);
    }
}

int main(int argc, char const *argv[])
{
    pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) //售票员
    {
        printf("i am saler\n");
        signal(SIGINT, saler);
        signal(SIGQUIT, saler);
        signal(SIGUSR1, saler);
        signal(SIGTSTP, SIG_IGN);
    }
    else //司机
    {
        printf("i am driver\n");
        signal(SIGUSR1, driver);
        signal(SIGUSR2, driver);
        signal(SIGTSTP, driver);
        signal(SIGINT, SIG_IGN);
        signal(SIGQUIT, SIG_IGN);
    }
    while (1)
        pause();  //不能只发送一个捕获模式处理的信号就结束进程,所以可以循环挂起,不占用CPU

    return 0;
}


4. 共享内存

4.1 特点

1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程
将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
3) 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

image.png

image.png


4.2 步骤

(1) 创建key值
(2) 创建或打开共享内存
(3) 映射共享内存到用户空间
(4) 撤销映射
(5) 删除共享内存

key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1
      
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL|0777
返回值:成功  shmid
      出错    -1
//当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。若已有该块共享内存,则返回-1。

void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么有用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
       出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)

int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1

int  shmctl(int  shmid,int  cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
    shmid   共享内存的id号
    cmd     IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可
    buf    shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0 
     失败-1
用法:shmctl(shmid,IPC_RMID,NULL);

4.3 函数接口

获取key值:

image.png


4.4. 命令:

ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存
ps: 可能不能直接删除掉还存在进程使用的共享内存。
这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。

image.png


通过共享内存进程间通信例子:
启动两个进程,利用相同key使用相同的共享内存从而通信

image.png

image.png


5. 信号灯集

线程:全局变量、同步通过信号量
初始化:sem_init(&sem,0,0);
申请资源:sem_wait(&sem); P操作,-1
释放资源:sem_post(&sem); V操作 +1

5.1 特点

信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;
而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)
System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。

5.2 步骤

(1) 创建key值: ftok
(2) 创建或打开信号灯集: semget
(3) 初始化信号灯: semctl
(4) PV操作: semop
(5) 删除信号灯集: semctl

5.3 命令

ipcs -s: 查看系统中的信号灯集
ipcrm -s semid: 删除指定的信号灯集
注意:有时候可能会创建失败,或者semid为0,所以用命令查看,删除重新创建就可以了。

5.4 函数接口

int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT|IPC_EXCL|0666
返回值:成功:信号灯集ID
       失败:-1
       
int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始
     cmd: 
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
      失败 -1

用法:
1. 初始化信号灯集:
需要自定义共用体
union semun
{
    int val;
};
union semun mysemun;
mysemun.val=10;
semctl(semid,0,SETVAL,mysemun);
2. 获取信号灯集值:函数semctl(semid,0,GETVAL);的返回值
3.删除信号灯集: semctl(semid,0,IPC_RMID); //

int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
     opsptr:操作方式
     nops:  要操作的信号灯的个数 1个
返回值:成功 :0
      失败:-1
struct sembuf {
   short  sem_num; // 要操作的信号灯的编号
   short  sem_op;  //    0 :  等待,直到信号灯的值变成0
                   //   1  :  释放资源,V操作
                   //   -1 :  申请资源,P操作                    
    short  sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};

用法:
申请资源P操作:
struct sembuf mysembuf;
mysembuf.sem_num=0
mysembuf.sem_op=-1;
mysembuf.sem_flg=0;
semop(semid,&mysembuf,1);
释放资源操作:
mysembuf.sem_num=1;
mysembuf.sem_op=1;
mysembuf.sem_flg=0;
semop(semid,&mysembuf,1);

创建或打开信号灯集:

image.png


注意:如果创建失败或semid为0,删除0重新执行程序就可以了。
信号灯的操作

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

union semun {
    int val;
};

int main(int argc, char const *argv[])
{
    int semid;
    key_t key;
    key = ftok("sem.c", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //创建或打开信号灯集
    semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); //创建信号灯集
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666); //直接打开已经有的信号灯集
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else //确保对信号灯集中的信号灯始终初始化一次,下次执行程序还继续获得上一次的值。如果修改初值,需要删除信号灯然后重新打开。
    {
        //初始化信号灯集
        union semun sem;
        sem.val = 10;
        semctl(semid, 0, SETVAL, sem);

        sem.val = 0;
        semctl(semid, 1, SETVAL, sem);
    }

    printf("semid: %d\n", semid);

    //获取信号灯的值
    printf("%d\n", semctl(semid, 0, GETVAL));
    printf("%d\n", semctl(semid, 1, GETVAL));

    //PV操作
    struct sembuf mysembuf;
    mysembuf.sem_num = 0;
    mysembuf.sem_op = -1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1); //v把0号灯进行P操作 -1

    mysembuf.sem_num = 1;
    mysembuf.sem_op = 1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1); //把1号灯进行V操作 +1

    //获取信号灯的值
    printf("%d\n", semctl(semid, 0, GETVAL));
    printf("%d\n", semctl(semid, 1, GETVAL));

    //删除信号灯集
    //semctl(semid,0,IPC_RMID); //指定任意一个信号灯的编号就可以删除整个灯集

    return 0;
}


函数操作

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

union semun {
    int val;
};

void init(int semid, int num, int val)
{
    union semun sem;
    sem.val = val;
    semctl(semid, num, SETVAL, sem); //对编号为num的灯设初值为val
}

void pv(int semid, int num, int op)
{
    struct sembuf mysembuf;
    mysembuf.sem_num = num;
    mysembuf.sem_op = op;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
}

int main(int argc, char const *argv[])
{
    int semid;
    key_t key;
    key = ftok("sem.c", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //创建或打开共享内存
    


    //创建或打开信号灯集
    semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); //创建信号灯集
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666); //直接打开已经有的信号灯集
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else //确保对信号灯集中的信号灯始终初始化一次,下次执行程序还继续获得上一次的值。如果修改初值,需要删除信号灯然后重新打开。
    {
        //初始化信号灯集
        init(semid, 0, 10); //对0号灯初始化为10
        init(semid, 1, 0);  //对1号灯初始化为0
    }
    printf("semid: %d\n", semid);

    //获取信号灯的值
    printf("%d\n", semctl(semid, 0, GETVAL));
    printf("%d\n", semctl(semid, 1, GETVAL));

    //PV操作
    pv(semid, 0, -1);
    pv(semid, 1, 1);

    //获取信号灯的值
    printf("%d\n", semctl(semid, 0, GETVAL));
    printf("%d\n", semctl(semid, 1, GETVAL));

    //删除信号灯集
    //semctl(semid,0,IPC_RMID); //指定任意一个信号灯的编号就可以删除整个灯集

    return 0;
}


把信号灯集添加到共享内存中
input.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>

union semun {
    int val;
};

void init(int semid, int num, int val)
{
    union semun sem;
    sem.val = val;
    semctl(semid, num, SETVAL, sem); //对编号为num的灯设初值为val
}

void pv(int semid, int num, int op)
{
    struct sembuf mysembuf;
    mysembuf.sem_num = num;
    mysembuf.sem_op = op;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
}

int main(int argc, char const *argv[])
{
    char *p;
    int shmid;
    int semid;
    key_t key;
    key = ftok("sem.c", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //创建或打开共享内存
    shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid <= 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0666);
        else
        {
            perror("shmget err");
            return -1;
        }
    }

    //映射共享内存
    p = (char *)shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }

    //创建或打开信号灯集
    semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); //创建信号灯集
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666); //直接打开已经有的信号灯集
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else //确保对信号灯集中的信号灯始终初始化一次,下次执行程序还继续获得上一次的值。如果修改初值,需要删除信号灯然后重新打开。
    {
        //初始化信号灯集
        init(semid, 0, 0); //对0号灯初始化为10
        init(semid, 1, 1); //对1号灯初始化为0
    }
    printf("semid: %d\n", semid);

    while (1)
    {
        pv(semid, 1, -1);
        scanf("%s", p);
        pv(semid, 0, 1);
        if (strcmp(p, "quit") == 0)
            break;
    }

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID); //指定任意一个信号灯的编号就可以删除整个灯集

    return 0;
}


output.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>

union semun {
    int val;
};

void init(int semid, int num, int val)
{
    union semun sem;
    sem.val = val;
    semctl(semid, num, SETVAL, sem); //对编号为num的灯设初值为val
}

void pv(int semid, int num, int op)
{
    struct sembuf mysembuf;
    mysembuf.sem_num = num;
    mysembuf.sem_op = op;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
}

int main(int argc, char const *argv[])
{
    char *p;
    int shmid;
    int semid;
    key_t key;
    key = ftok("sem.c", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //创建或打开共享内存
    shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid <= 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0666);
        else
        {
            perror("shmget err");
            return -1;
        }
    }

    //映射共享内存
    p = (char *)shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }

    //创建或打开信号灯集
    semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); //创建信号灯集
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666); //直接打开已经有的信号灯集
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else //确保对信号灯集中的信号灯始终初始化一次,下次执行程序还继续获得上一次的值。如果修改初值,需要删除信号灯然后重新打开。
    {
        //初始化信号灯集
        init(semid, 0, 0); //对0号灯初始化为10
        init(semid, 1, 1); //对1号灯初始化为0
    }
    printf("semid: %d\n", semid);

    while (1)
    {
        pv(semid, 0, -1);
        if (strcmp(p, "quit") == 0)
            break;
        printf("%s\n", p);
        pv(semid, 1, 1);
    }

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID); //指定任意一个信号灯的编号就可以删除整个灯集

    return 0;
}


6. 消息队列

传统: 无名管道、有名管道、信号
system V: 共享内存、信号灯集、消息队列
按消息的类型添加或读取消息
遵循队列原则

6.1 特点

消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种
一个消息队列由一个标识符 (即队列ID)来标识
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息

6.2 步骤

(1) 产生key值 ftok()
(2) 创建或打开消息队列 msgget()
(3) 添加消息:按照消息类型把消息添加到已经打开的消息队列的末尾msgsnd()
(4) 读取消息:可以按照把消息类型从消息队列中取走msgrcv()
(5) 删除消息队列 msgctl()

6.3 命令

ipcs -q: 查看消息队列
ipcrm -q msgid: 删除指定的消息队列

6.4 函数接口

int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:  key值
       flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
       失败:-1

int msgsnd(int msqid, const void *msgp, size_t size, int flag); 
功能:添加消息
参数:msqid:消息队列的ID
      msgp:指向消息的指针。常用消息结构msgbuf如下:
          struct msgbuf{
            long mtype;          //消息类型
            char mtext[N]};     //消息正文,自己指定
   size:发送的消息正文的字节数sizeof(msg)-sizeof(long)
   flag:IPC_NOWAIT消息没有发送完成函数也会立即返回    
         0:直到发送完成函数才返回
返回值:成功:0
      失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);
功能:读取消息
参数:msgid:消息队列的ID
     msgp:存放读取消息的空间
     size:接受的消息正文的字节数(sizeof(msg)-sizeof(long))
    msgtype:
            0:接收消息队列中第一个消息。
            大于0:接收消息队列中第一个类型为msgtyp的消息.
            小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
     flag:
           0:若无消息函数会一直阻塞
           IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
      失败:-1
      
  
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
     cmd:
        IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        IPC_SET:设置消息队列的属性。这个值取自buf参数。
        IPC_RMID:从系统中删除消息队列。
     buf:消息队列缓冲区
返回值:成功:0
      失败:-1
用法:msgctl(msgid, IPC_RMID, NULL);

创建或打开消息队列:

image.png


添加读取消息:

image.png


操作消息队列:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

struct msgbuf
{
    long type; //必须有并且是第一个,表示消息的类型,值>0
    int num;   //正文按需求随意定义
    char ch;
};

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    if ((key = ftok("msg.c", 'c')) < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key: %#x\n", key);

    //打开消息队列
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid <= 0)
    {
        if (errno == EEXIST)
            msgid = msgget(key, 0666);
        else
        {
            perror("msgget err");
            return -1;
        }
    }
    printf("msgid: %d\n", msgid);

    //添加消息
    struct msgbuf msg;
    msg.type = 1;
    msg.num = 100;
    msg.ch = 'a';
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    msg.type = 2;
    msg.num = 200;
    msg.ch = 'b';
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    //读取消息
    struct msgbuf m;
    msgrcv(msgid, &m, sizeof(m) - sizeof(long), 1, 0);
    printf("%d %c\n", m.num, m.ch);

    msgrcv(msgid, &m, sizeof(m) - sizeof(long), 2, 0);
    printf("%d %c\n", m.num, m.ch);

    //删除消息队列
    msgctl(msgid,IPC_RMID,NULL);

    return 0;
}


两个进程收发消息:

image.png

image.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值