• 5、程序、进程、线程(一)


    程序、进程、线程

    进程产生的方式

    进程号

    每个进程在初始化的时候系统都分配了一个ID号,在LInux中进程号是唯一的,描述进程的ID号通常叫做PID(process ID),PID的变量类型为pid_t

    getpid(), getppid()函数介绍

    getpid函数返回当前进程ID。getppid返回当前进程的父进程的ID。类型pid_t其实是一个typedef类型,定义为unsigned int。

    头文件 sys/types.h unistd.h

    函数原型

    pid_t getpid(void);
    pid_t getppid(void)
    
    • 1
    • 2
    进程复制 fork函数

    头文件 sys/types.h unistd.h

    函数原型

    pid_t fork(void);
    
    • 1

    fork函数的返回值是进程的ID;失败返回-1

    fork的特点是执行一次,返回两次。在父进程和子进程中的返回值是不一样的,父进程中返回的是子进程的ID号,而子进程中则返回0。

    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        pid_t pid, ppid;
        pid = fork();
        if(pid == -1) {
            puts("产生进程失败");
        }
        else {
            if(pid == 0) {
                puts("这是子进程");
                printf("子进程fork返回值 : %u\n",pid);
                pid = getpid();
                ppid = getppid();
                printf("子进程的ID为 %u\n",pid);
                printf("子进程父进程的ID为 %u\n",ppid);
            }
            else {
                puts("这是父进程");
                printf("父进程fork返回值 : %u\n",pid);
                pid = getpid();
                ppid = getppid();
                printf("父进程的ID为 %u\n",pid);
                printf("父进程的父进程的ID为 %u\n",ppid);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    发现子进程的父进程的ID有时候是产生子进程的那个进程的ID,有时候却是 1。

    system方式

    system函数调用shell的外部命令在当前进程中开始另一个进程(这个函数是C语言中的函数)

    头文件stdlib.h

    函数原型

    int system(const char *command);
    
    • 1

    system函数调用“/bin/sh-c command”执行特定的命令,阻塞当前进程直到command执行完毕。

    执行system函数的时候会调用fork、execve、waitpid等函数,其中任意一个调用失败将导致system函数调用失败。system函数的返回值如下:

    • 失败返回-1
    • 当sh不能执行时返回127
    • 成功,返回进程状态值

    进程执行exec函数系列

    在使用fork和system函数的时候,系统中都会建立一个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显示的退出。而exec族的函数与之前的fork和system函数不一样,exec函数会用新进程代替原有的进程,系统会从新进程开始运行,新进程的PID与原进程的PID相同。

    exec函数族介绍

    头文件 unistd.h

    函数原型

    extern char **environ;
    
    int execl(const char *path, const char *arg, .../* (char  *) NULL */);
    int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
    int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述六个函数在linux man手册中都是属于(3)类型的,也就是库函数,其实他们都是对execve这个系统函数的封装。

    execve函数

    头文件 unistd.h

    函数原型

    int execve(const char *filename, char *const argv[], char *const envp[]);
    
    • 1

    上述exec函数族的作用是:在当前系统中根据指定的文件路径寻找可执行文件并用它来取代调用进程的内容。即在原来的进程内部执行一个可执行文件,这个文件既可以是二进制文件,也可以是可执行的脚本文件。

    与fork函数不同,exec函数执行成功后不会返回,这是因为当前进程的空间和资源已经被占用了,这些资源包括代码段、数据段和堆栈等。它们都已经被新内容取代,而进程的ID等标识性的东西任然是原来的东西,即exec函数在原来进程的壳上运行了自己的东西。只有程序运行失败了系统才会返回-1。

    使用exec函数的一种普遍的方法是先使用fork函数分叉进程,然后在新的进程中调用exec函数。linux系统针对上述过程专门进行了优化。由于fork的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在fork系统调用之后进程exec系统调用,系统就不会进行系统复制,而是直接使用exec指定的参数来覆盖原有的进程。上述的方法在linux系统上叫做写时复制(copy ont write)

    ,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。

    execve函数的例子
    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        pid_t pid ;
        pid = getpid();
        printf("进程的PID为 %d\n",pid);
        if(execve("/bin/ls",argv, NULL) < 0) {
            puts("创建进程出错");
        }
        puts("这里还会执行吗?");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    所有用户态进程的产生进程init

    在linux系统中所有进程都有父子或者堂兄第关系的,除了初始进程init,没有那个进程与其他进程完全独立。系统中每个进程都有一个父进程,新的进程不是被全新的创建,通常是从一个原有的进程进行复制或者克隆的。可以使用命令pstree查看系统中运行的进程之间的关系。我发现我的最初始的进程是systemd而不是init

    网上搜了一下,systemd取代了init。参考资料

    进程间通信与同步

    在linux下多个进程之间的通信机制叫做IPC,它是多个进程之间相互沟通的一种方法。在Linux下有多种进程间的通信方法:半双工管道、FIFO(命名管道)、消息队列、信号量、共享内存等。使用这些通信机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。

    半双工管道

    管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的方法,自unix操作系统诞生,管道就存在了。

    由于管道仅仅是将某个进程的输出与另一个进程的输入相连接的单向通信的方法,因此称其为半双工。在shell中管道用 |表示,例如ls -l | grep *.c表示将ls -l的输出作为grep *.c的输入。管道在前一个进程中建立输入通道,在后一个进程中建立输出通道。

    进程创建管道每次创建两个文件描述符来操作管道,其中一个作为输入,另一个作为输出。

    在这里插入图片描述

    把管道想象成一个文件。对管道的读写与一般的IO系统函数一致,使用write函数写入数据,read函数读出数据,某些特定的IO操作管道是不支持的,例如偏移函数lseek。

    pipe函数介绍

    创建管道的函数原型为

     #include 
    
    int pipe(int pipefd[2]);
    
    • 1
    • 2
    • 3

    pipefd是一个文件描述符的数组,用于返回管道保存的两个文件描述符,数组中第一个元素(下标为0)是为了读操作而创建和打开的,第二个元素是为了写元素而创建和打开的。pipe函数执行成功时返回0,失败时返回-1。

    只建立管道似乎没什么用,要使管道有切实的用处,需要与进程的创建结合起来,利用两个管道在父进程和子进程之间进行通信。

    在这里插入图片描述

    要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。

    pipe函数的例子

    为了方便阅读,创建两个指针write_fd , read_fd分别指向fd[1]和fd[0]。

    #include
    #include
    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        int result = -1;//存储创建管道的结果
        int fd[2];
        pid_t pid;
        int *write_fd = &fd[1];
        int *read_fd = &fd[0];
    
        result = pipe(fd);
        if(result == -1) {
            puts("管道创建失败");
            return -1;
        }
        pid = fork();//分叉程序
        if(pid == -1) {
            puts("进程fork失败");
            return -1;
        }
        if(pid == 0) {//子进程
            close(*read_fd);//关闭读
            //向管道写入数据
            write(*write_fd, "你好,管道", strlen("你好,管道"));
    
            return 0;
        }
        else {
            close(*write_fd);//关闭写
            //从管道读取数据
            char *buf = (char*)malloc(100);
            memset(buf, 0, 100);
            read(*read_fd, buf, 100);
            printf("从管道收到的数据为 :%s\n",buf);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    试试不在父进程中关闭写 : 还是能正常接收到数据

    试试不在子进程中关闭读:还是能正常接收到数据

    试试同时不关闭读和写:仍然能接收到数据

    看来这个关闭只是为了模型上的匹配,对实际没有影响。

    #include
    #include
    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        int result = -1;//存储创建管道的结果
        int fd[2];
        pid_t pid;
        int *write_fd = &fd[1];
        int *read_fd = &fd[0];
    
        result = pipe(fd);
        if(result == -1) {
            puts("管道创建失败");
            return -1;
        }
        pid = fork();//分叉程序
        if(pid == -1) {
            puts("进程fork失败");
            return -1;
        }
        if(pid == 0) {//子进程
            close(*read_fd);//关闭读
            //向管道写入数据
            char buf[100];
            puts("输出你想传送到父进程的消息:");
            while(scanf("%s",buf) != 0) {
                write(*write_fd, buf, strlen(buf));
                puts("输出你想传送到父进程的消息:");
            }
    
            return 0;
        }
        else {
            close(*write_fd);//关闭写
            //从管道读取数据
            char *buf = (char*)malloc(100);
            memset(buf, 0, 100);
            
            while(1) {
                read(*read_fd, buf, 100);
                printf("从管道中接收到的消息为: %s\n",buf);
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    管道阻塞和管道操作的原子性

    命名管道

    • 在文件系统中命名管道是以设备特殊文件的形式存在的
    • 不同的进程可以通过命名管道共享数据
    创建FIFO

    有多种方式可以创建命名管道,例如使用shell直接创建:mkfifo namedfifo

    ls -l 可以看到文件属性为prw-rw-r-- 1 luoxin luoxin 0 8月 20 09:19 namedfifo前面的p代表这是一个pipe(管道)

    或者使用C语言创建,需要用到mkfifo函数

    #include 
    #include 
    
    int mkfifo(const char *pathname, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4
    FIFO操作

    对于FIFO来说,IO操作与普通管道的IO操作基本上是一致的,两者之间存在一个主要的区别:在FIFO中必须使用一个open函数来显式的建立连接到管道的通道。一般来说FIFO总是处于阻塞状态,也就是说如果FIFO打开时设置了读权限,则读进程将一致阻塞,直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开管道写入数据,当没有进程从管道中读取数据的时候,写操作的管道也是阻塞的,直到已经写入的数据被读取后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在open调用中使用O_NONBLOCK标志,以关闭默认的阻塞动作。

    //下面的代码验证读进程会因为写进程而阻塞

    #include
    #include
    #include
    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        pid_t pid = fork();
        if(pid == -1) {
            puts("进程分叉失败");
            return -1;
        }
        if(pid == 0) {//子进程
            //打开管道并进行写操作
            char str[] = "hello";
            int write_fd = open("/tmp/ipc/namedfifo", O_WRONLY);
            puts("before write");
            sleep(2);//休眠两秒,以验证读进程会阻塞
            write(write_fd, str, strlen(str));
            puts("after write");
            close(write_fd);
        }
        else {//父进程
            //打开管道并读取
            char buf[100];
            int read_fd = open("/tmp/ipc/namedfifo", O_RDONLY);
            memset(buf, 0, 100);
            read(read_fd, buf,100);
            printf("从管道中获取到的数据为: %s\n",buf);
            close(read_fd);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    //下面的代码验证写进程会因为没有读进程读取数据而阻塞但是失败了,不知道是我理解错了书上的意思还是说系统有所改动。

    #include
    #include
    #include
    #include
    #include
    #include
    
    int main(int argc, char *argv[]) {
        pid_t pid = fork();
        if(pid == -1) {
            puts("进程分叉失败");
            return -1;
        }
        if(pid == 0) {//子进程
            //打开管道并进行写操作
            char str[] = "hello";
            int write_fd = open("/tmp/ipc/namedfifo", O_WRONLY);
            puts("before write1");
            //sleep(2);//休眠两秒,以验证读进程会阻塞
            write(write_fd, str, strlen(str));
            puts("after write1");
    
            puts("before write2");
            write(write_fd, str, strlen(str));
            puts("after write2");
            
            close(write_fd);
        }
        else {//父进程
            //打开管道并读取
            char buf[100];
            int read_fd = open("/tmp/ipc/namedfifo", O_RDONLY);
            memset(buf, 0, 100);
            sleep(2);//休眠2秒,已以验证写进程会阻塞
            read(read_fd, buf,100);
            printf("从管道中获取到的数据为: %s\n",buf);
            close(read_fd);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    消息队列

    消息队列是内核地址空间中的内部链表,通过linux内核在各个进程之间传递内容。消息顺序的发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用IPC标识符唯一的标识。内核中的消息队列是通过IPC标识符来区分的,不同的消息队列之间是相互独立的,每个消息队列中的消息,又构成一个独立的链表。

    消息缓冲区结构

    常用的结构是msgbuf结构。程序员可以以这个结构为模板定义自己的消息结构,在头文件linux/msg.h中,它的定义如下:

    /usr/include/linux/msg.h中查看

    /* message buffer for msgsnd and msgrcv calls */
    struct msgbuf {
    	__kernel_long_t mtype;          /* type of message */
    	char mtext[1];                  /* message text */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    mtype:消息类型,设置消息类型以区分不同进程之间的消息

    mtext:消息内容,可以重写自己的消息扩大消息内容的长度,例如:

    struct my_msgbuf {
    	__kernel_long_t mtype;          /* type of message */
    	char mtext[10];                  /* message text */
    	int length;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    结构msgid__ds

    每个消息队列都维护了一个这样的结构。

    /* Obsolete, used only for backwards compatibility and libc5 compiles */
    struct msqid_ds {
    	struct ipc_perm msg_perm;
    	struct msg *msg_first;		/* first message on queue,unused  */
    	struct msg *msg_last;		/* last message in queue,unused */
    	__kernel_time_t msg_stime;	/* last msgsnd time */
    	__kernel_time_t msg_rtime;	/* last msgrcv time */
    	__kernel_time_t msg_ctime;	/* last change time */
    	unsigned long  msg_lcbytes;	/* Reuse junk fields for 32 bit */
    	unsigned long  msg_lqbytes;	/* ditto */
    	unsigned short msg_cbytes;	/* current number of bytes on queue */
    	unsigned short msg_qnum;	/* number of messages in queue */
    	unsigned short msg_qbytes;	/* max number of bytes on queue */
    	__kernel_ipc_pid_t msg_lspid;	/* pid of last msgsnd */
    	__kernel_ipc_pid_t msg_lrpid;	/* last receive pid */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    其中struct ipc_perm定义在/usr/include/linux/ipc.h

    /* Obsolete, used only for backwards compatibility and libc5 compiles */
    struct ipc_perm
    {
    	__kernel_key_t	key;
    	__kernel_uid_t	uid;
    	__kernel_gid_t	gid;
    	__kernel_uid_t	cuid;
    	__kernel_gid_t	cgid;
    	__kernel_mode_t	mode; 
    	unsigned short	seq;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    键值构造函数ftok
    #include 
    #include 
    
    key_t ftok(const char *pathname, int proj_id);
    
    • 1
    • 2
    • 3
    • 4

    其中pathname必须是已经存在的目录,而proj_id是一个8位的值。这个生产的key在msgget函数中需要,用于获取或者创建一个消息队列。

    获取消息队列msgget
    #include 
    #include 
    #include 
    
    int msgget(key_t key, int msgflg);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    从内核中获取与key相同的队列,比较后打开或者访问操作依赖于msgflg参数的内容。

    • IPC_CREAT:如果在内核中不存在该队列,则创建它。
    • IPC_EXCL:与IPC_CREAT一起使用时,如果队列早已存在则会出错。

    返回一个消息队列标识符。这个消息队列标识符在后面的发送消息和接受消息的函数中会使用到。

    发送消息msgsnd函数
    接收消息msgrcv函数
    #include 
    #include 
    #include 
    
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第一个参数是消息队列标识符,第二个参数是一个void型指针,指向一个消息缓冲区,用于发送或者接收消息。第三个参数是一个size型参数,对于发送来说就是发送的消息的size,对于接收来说就是期望接收的消息的大小。msgflag是标志参数。对于msgsnd来说:

    • 设置为0,表示忽略
    • 设置为IPC_NOWAIT,如果消息队列已满,则消息不会被写入到队列中,如果没有指定NOWAIT,则进程会被阻塞直到可以发送消息为止。

    对于msgrcv来说:

    • 设置位IPC_NOWAIT,当消息队列中没有任何消息时会返回ENOMSG而不是阻塞。

    如果在等待消息时队列被删除了,则返回EIDRM,

    其中msgrcv函数中的msgtyp参数用于指定消息的类型,这样才能达到精准通信的目的。

    消息控制函数msgctl

    msgctl可以直接对消息队列的内部结构进程控制。

    #include 
    #include 
    #include 
    
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第一个参数是队列标识符,指明要操控的队列,第三个参数是 msqid_ds结构体,用于设置或者接收队列的内部结构信息。第二个参数cmd可以为以下值:

    • IPC_STAT:获取队列的msqid_ds结构信息
    • IPC_SET:设置队列msqid_ds中ipc_perm成员的值(因为其他值都是最后修改时间之类的)
    • IPC_RMID:remove id ,内核删除队列。
    消息队列的例子

    信号量

    信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用来做一个锁机制。生产者和消费者模型是信号量的典型使用。

    信号量数据结构
    union semun {
    	int val;//整性变量
    	struct semid_ds *buf;//semid_ds型结构指针
    	unsigned short *array;//数组类型
    	struct seminfo *__buf;//信号量内部结构
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    新建信号量函数semget

    semget函数用于创建一个新的信号量集合或者访问现有的集合。

    #include 
    #include 
    #include 
    
    int semget(key_t key, int nsems, int semflg);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第一个参数是ftok生成的键值,第二个参数可以指定在新的集合中应该创建的信号量的数目。

    大概的意思就是第二个参数可以为0当信号量已经存在的时候,但是如果要新建信号量,则第二个参数必须是一个大于0 小于等于最大值的数。

    The argument nsems can be 0 (a don’t care) when a semaphore set is not being created. Otherwise, nsems must be greater than 0 and less than or equal to the maximum number of semaphores per semaphore set (SEMMSL).

    第三个参数是打开信号量的方式。可以是

    • IPC_CREAT
    • IPC_EXCL

    这两个参数前面已经讲了很多遍了,这里不再赘述。

    返回值是一个信号量描述符。

    下面封装的函数实现获取指定键值的信号量并设置其值为value。

    typedef int sem_t;
    union semun {
    	int val;//整性变量
    	struct semid_ds *buf;//semid_ds型结构指针
    	unsigned short *array;//数组类型
    }arg;
    
    sem_t createSem(key_t key, int value) {
        union semun sem;
        sem_t sem_id;
        sem.val = value;
    
        sem_id = semget(key, 0, IPC_CREAT | IPC_EXCL);
        if(sem_id == -1) {
            puts("创建信号量失败");
            return -1;
        }
        semctl(sem_id, 0, SETVAL, sem);
    
        return sem_id;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    信号量操作函数semop

    信号量的P、V操作是通过向已经建立好的信号量发送命令来完成的。向信号量发送命令的函数是semop。函数原型如下:

    #include 
    #include 
    #include 
    
    int semop(int semid, struct sembuf *sops, size_t nsops);
    
    int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第一个参数是信号量标识符,第二个参数是一个指向 需要在该信号量上执行的操作的数组 的指针。第三个参数是该数组中操作的个数,其实也就是参数二数组的长度。

    sembuf 的结构如下:

    unsigned short sem_num;  /* semaphore number */
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
    
    • 1
    • 2
    • 3
    • sem_num:用户要处理的信号量的编号。
    • sem_op:将要执行的操作(正数、负数或者 0 )
    • 信号量操作的标志 可以为 IPC_NOWAIT 或者 SEM_UNDO
    控制信号量参数–semctl函数
    #include 
    #include 
    #include 
    
    int semctl(int semid, int semnum, int cmd, ...);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    cmd取值范围为:

    • IPC_STAT:获取某个集合的semid_ds结构,并把他存储在semun联合体的__buf中
    • IPC_SET:设置某个sem_ds中ipc_perm的值
    • IPC_RMID:删除该集合
    • GETALL:获取集合中所有信号量的值,存储在联合体的array成员中
    • GETNCNT:返回当前正在等待资源的进程的数目
    • GETPID:返回最后一个调用semop的进程的pid
    • GETVAL:返回集合中某个信号量的值
    • GETZCNT:返回正在等待资源利用率达到百分百的进程的数目
    • SETALL:把集合中所有信号量的值设置为联合体中array成员中对应的值
    • SETVAL:把集合中单个信号量的值设置为成员中val对应的值

    可以在第四个参数的时候传入一个semun联合体

    共享内存

    共享内存是通过在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式。

    创建共享内存函数shmget(shared memory get)
    #include 
    #include 
    
    int shmget(key_t key, size_t size, int shmflg);
    
    • 1
    • 2
    • 3
    • 4

    shmflg:

    • IPC_CREAT
    • IPC_EXCL

    返回一个共享内存的描述符

    获得共享内存地址的函数shmat(shared memory attaches)
    删除共享内存的函数shmdt(shared memory detaches)
    #include 
    #include 
    
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    
    int shmdt(const void *shmaddr);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果shmaddr等于0 ,则内核将尝试找一个未映射的区域。用户可以指定一个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。SHM_RND标志可以与标志参数进行OR操作,这样可以让传递的地址页对齐。SHM_RDONLY打开的共享内存只读。

    共享内存控制函数shmctl
    #include 
    #include 
    
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    • 1
    • 2
    • 3
    • 4

    shmid_ds:

    struct shmid_ds {
        struct ipc_perm shm_perm;    /* Ownership and permissions */
        size_t          shm_segsz;   /* Size of segment (bytes) */
        time_t          shm_atime;   /* Last attach time */
        time_t          shm_dtime;   /* Last detach time */
        time_t          shm_ctime;   /* Last change time */
        pid_t           shm_cpid;    /* PID of creator */
        pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        shmatt_t        shm_nattch;  /* No. of current attaches */
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    信号

    信号是unix最为古老的进程之间进行通信的机制。它用于在一个或者多个进程之间传递异步信号。

    linux定义了一系列的信号,这些信号可以由内核产生,也可以由系统中其他进程产生,只要这些进程有足够的权限。可以使用kill -l

    列出机器上所有的信号

    luoxin@luoxin-virtual-machine:~$ kill -l
     1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
     6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
    11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
    16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
    21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
    26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
    31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
    38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
    43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
    48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
    53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
    58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
    63) SIGRTMAX-1	64) SIGRTMAX
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    进程可以屏蔽掉大多数的信号,除了SIGSTOP 和SIGKILL,一个是暂停信号,一个是退出信号。

    信号截取函数signal
    #include 
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    向进程发送信号函数kill,raise
    luoxin@luoxin-virtual-machine:~$ man -f kill
    kill (1)             - send a signal to a process
    kill (2)             - send signal to a process
    luoxin@luoxin-virtual-machine:~$ man -f raise
    raise (3)            - send a signal to the caller
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include 
    #include 
    //向某个进程发送信号,如果pid为 0 时向所有进程发送信号
    int kill(pid_t pid, int sig);
    
    • 1
    • 2
    • 3
    • 4
    #include 
    //向当前进程发送信号
    int raise(int sig);
    
    • 1
    • 2
    • 3

    ) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
    63) SIGRTMAX-1 64) SIGRTMAX

    
    进程可以屏蔽掉大多数的信号,除了SIGSTOP 和SIGKILL,一个是暂停信号,一个是退出信号。
    
    #### 信号截取函数signal
    
    ```c
    #include 
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    向进程发送信号函数kill,raise
    luoxin@luoxin-virtual-machine:~$ man -f kill
    kill (1)             - send a signal to a process
    kill (2)             - send signal to a process
    luoxin@luoxin-virtual-machine:~$ man -f raise
    raise (3)            - send a signal to the caller
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include 
    #include 
    //向某个进程发送信号,如果pid为 0 时向所有进程发送信号
    int kill(pid_t pid, int sig);
    
    • 1
    • 2
    • 3
    • 4
    #include 
    //向当前进程发送信号
    int raise(int sig);
    
    • 1
    • 2
    • 3
  • 相关阅读:
    vue超好用的自定义指令封装
    【自然语言处理】面向新冠肺炎的社会计算应用
    sora会是AGI的拐点么?
    【Python知识】谈谈Python的抽象类
    RK3399应用开发 | 移植libdrm到rk3399开发板(2.4.113)
    适用于车载设备无钥匙进入系统汽车用晶振FA-238A
    【网络安全】利用samba服务绕过未开启文件包含配置
    Python机器视觉--OpenCV进阶(核心)--图像金字塔(高斯金字塔与拉普拉斯金字塔)
    性能优化实战使用CountDownLatch
    mapbox鼠标滑过高亮要素
  • 原文地址:https://blog.csdn.net/m0_45972156/article/details/126452463