嵌入式开发——IO进程

该文为学习笔记,仅作学习参考,如有错误,望指正!

一、标准IO

1. IO

  1. IO:input、output(读写操作——文件);
  2. IO的方式:标准IO、文件IO;
  3. 标准IO:间接采用系统调用(采用库函数)的方式对文件实现读写操作;
  4. 文件IO: 直接采用系统调用的方式对文件(设备)实现读写操作;
  5. 系统调用层:内核维护的接口层,用户程序可以通过该接口层实现各种功能;
  6. 封装函数(func)-----》系统调用接口(中断)------》syscall(编号)-----》sys_func(实现功能);
  7. CPU的状态切换 用户态–》内核态;
  8. 每一个系统调用的函数接口都会对应一个编号,即syscall;
  9. 参数传递:将参数传递给cpu内部的寄存器,运行sys_func之前将参数从寄存器中传到内核进程的堆栈区;

在这里插入图片描述

2. 标准IO

  1. 标准IO: 当对文件进行操作时,首先操作缓冲区,等到缓冲区满足一定的条件(缓冲区满或刷新缓冲区)时,然后再去执行系统调用,实现对文件的操作;
  2. 标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用FILE *来描述。
  3. FILE指针:每个被使用的文件都存在;
  4. 内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为 FILE;
  5. char* _IO_buf_base; /* Start of reserve area. */ 缓冲区的开始;
  6. char* _IO_buf_end; /* End of reserve area. */ 缓冲区的末尾;
  7. 缓冲区的大小:sizeof(_IO_buf_end)-sizeoof(_IO_buf_base)
#include <stdio.h>
int main(int argc, const char *argv[])
{
	FILE *fp;
	fp = fopen("test.txt", "w");
	fputc('a', fp);
	printf("size = %d\n", fp->_IO_buf_end - fp->_IO_buf_base);
	getchar();
	printf("size = %d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
	printf("size = %d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
	printf("size = %d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
	return 0;
}
[root@192 learn]# ./a.out 
size = 4096
w
size = 1024
size = 1024
size = 0

2.1. fputc函数原型

函数原型:int fputc(int c, FILE *stream);

  1. 功能:向文件写入一个字符;
  2. 参数:c 写入的字符;stream 流指针;
  3. 返回值:成功返回已经写入的字符(强转);失败 EOF;
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
	FILE *fp;	
	int i;
	if((fp = fopen("test.txt", "w")) == NULL){
		printf("fopen error:%s\n", strerror(errno));
		return -1;
	}
	for(i = 0; i < 5; i++){
		if(fputc(99, fp) == EOF){              //fputc为存入字符,99以ASCII码形式存入
			perror("fputc error");
			return -1;
		}
	}
	return 0;
}

2.2. fgetc函数原型

函数原型:int fgetc(FILE *stream);

  1. 功能:从指定的文件中读取一个字符;
  2. 参数:stream 文件流指针;
  3. 返回值:成功 读取的字符;失败 EOF;
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(){
    FILE *fp;
    char ch;
    if ((fp = fopen("test.txt", "r")) == NULL)
    {
        printf("fopen error:%s\n", strerror(errno));
        return -1;
    }
    if ((ch = (fgetc(fp))) == EOF)		// fgetc 直接读取文件流指针
    {
        perror("fgetc error");
        return -1;
    }
    printf("ch = %c\n", ch);
    return 0;
}

2.3. fgets函数原型

实现对文件操作:采用字符形式、采用字符串的形式、采用二进制的形式。

函数原型:FILE *fgets (char *s, int size, FILE *stream );

  1. 功能:从指定的文件中读取一个字符串,并保存到字符数组中;

  2. 参数:s 字符数组;size 需要读取的字符数目;stream 文件流指针;

  3. 返回值:读取成功 返回字符数组首地址,也即 s;读取失败 返回 NULL;

    如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL;

2.4. fputs函数原型

函数原型:FILE *fgets (char *s, FILE *stream );

  1. 功能:字符串写入到指定的流 stream 中,但不包括空字符。;
  2. 参数:s 字符数组,包含了要写入的以空字符终止的字符序列;stream 文件流指针;
  3. 返回值:成功 函数返回一个非负值,错误 则返回 EOF。

2.5. fopen函数原型

函数原型:FILE *fopen(const char path, const char mode);

  1. 功能:打开文件;

  2. 参数:

    path 文件名;

    mode 进程打开文件的方式;

    1. r:以只读的方式打开文件,如果文件不存在,错误;
    2. r+:以读写的方式打开文件,如果文件不存在,错误;
    3. w:以只写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,清空文件中的数据;
    4. w+:以读写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,清空文件中的数据;
    5. a:以只写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,写入文件的末尾;
    6. a+:以读写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,追加到文件的末尾;
  3. 返回值:成功 流指针; 失败 NULL;

实例:若文件打开错误输出报错信息

#include <stdio.h>
#include <errno.h>       //printf函数输出调用strerror需要加头文件 
#include <string.h>      //printf函数输出调用stderr需要加头文件 
int main(int argc, char *argv[])
{
	FILE *fp;
	if((fp = fopen("test.txt", "r")) == NULL)
    {
//		perror("fopen error");
		printf("fopen error:%s\n", strerror(errno));
		fprintf(stderr, "fopen error\n");
		return -1;
	}
	return 0;
}

实例:file内容每4个一行输出

#include <stdio.h>

#define Size 5

int main(){
    FILE *fp;
    char buf[Size] = "";
    if ((fp = fopen("test.txt", "r")) == NULL)
    {
        perror("fopen error!");
        return -1;
    }
    while ((fgets(buf, Size, fp)) != NULL)
    {
        printf("buf = %s\n", buf);
    }
    return 0;
}

实例:hello world内容赋值给text.txt

#include <stdio.h>

int main(){
    FILE *fp;
    char buf[] = "Hello World!";
    if ((fp = fopen("test.txt", "w")) == NULL)
    {
        perror("fopen error!");
        return -1;
    }
    if ((fputs(buf, fp)) == EOF)		//向文件里写入字符串时不用规定大小,每次写入遇到'\n'或size-1个字符结束
    {
        perror("fputs error!");
        return -1;
    }
    return 0;
}

实例:输出文件行数

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

int GetLine(FILE *fp){
    int count = 1;
    char ch = fgetc(fp);
    if (ch == EOF)
        count = 0;
    while ((ch = (fgetc(fp))) != EOF)
    {
        if (ch == '\n')
            count++;
    }
    return count;
}

int main(){
    FILE *fp;
    if ((fp = fopen("test.txt", "r")) == NULL)
    {
        perror("fopen error!");
        return -1;
    }
    int line = GetLine(fp);
    printf("line = %d\n", line);
    return 0;
}

实例:命令行传参,求出任意一个文件行数

#include <stdio.h>
#include <string.h>
int GetLine(FILE *fp){
    int count = 1;
    char ch = fgetc(fp);
    if (ch == EOF)
        count = 0;
    while ((ch = (fgetc(fp))) != EOF)
    {
        if (ch == '\n')
            count++;
    }
    return count;
}
int main(int argc, char *argv[]){
    FILE *fp;
    if ((fp = fopen(argv[1], "r")) == NULL)
    {
        perror("fopen error!");
        return -1;
    }
    int line = GetLine(fp);
    printf("line = %d\n", line);
    return 0;
}

2.6. fseek函数原型

  1. 函数原型:int fseek(FILE *stream, long offset, int whence);

  2. 功能:定位当前的读写位置;

  3. 参数:

    stream 文件流指针;

    offset为偏移量,在第三个参数定位的基础上产生位置的偏移;

    whence SEEK_SET 将文件的读写位置定位到文件的开始处,SEEK_CUR将文件的读写位置定位到文件当前位置,SEEK_END将文件的读写位置定位到文件的末尾处;

  4. 返回值:成功 返回 0,失败 返回 -1;

2.7. ftell函数原型

函数原型:long ftell(FILE *stream);

  1. 功能:获取当前读写位置的值;
  2. 参数:stream 文件流指针;
  3. 返回值:成功 返回位置的值(偏移量-相对于文件开始处的偏移量),失败 返回 -1;
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
	FILE *fp;	
	int ch;
	if((fp = fopen("test.txt", "w+")) == NULL){
		printf("fopen error:%s\n", strerror(errno));
		return -1;
	}
    fputc('a', fp);          //从0位置输入‘a’后,当前位置为1
	fputc('b', fp);          //当前位置为2
	fputc('c', fp);          //当前位置为3
	fputc('d', fp);          //当前位置为4
	fseek(fp, -4, SEEK_CUR);      //向左偏移4个单位后为0
	long offset;
	offset = ftell(fp);
	printf("offset1 = %ld\n", offset);
	fputc('e', fp);           //e覆盖a的位置,光标向后移动一位,当前位置为1
	fseek(fp, 2, SEEK_CUR);         //向右移动2个单位,当前位置为3
	offset = ftell(fp);
	printf("offset2 = %ld\n", offset);
	return 0;
}
[root@192 learn]# ./a.out 
offset1 = 0
offset2 = 3

2.8. fwrite函数原型

函数原型: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

  1. 功能:向文件中写入nmemb个数据单元;
  2. 参数:ptr 数据的来源,size 每个单元数据的大小,nmemb 写入文件的单元数据的个数,stream 流指针;
  3. 返回值:成功 返回写入的数据单元的个数;失败 返回3负数 ;

2.9. fread函数原型

函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

  1. 功能:从文件中读取nmemb个数据单元;
  2. 参数:ptr 保存读取的数据,size 数据单元的大小,nmemb 读取数据单元的个数,stream 流指针;
  3. 返回值:成功为读取单元的个数;失败为负数;

3. 预定义流指针

标准I/O预定义3个流,他们可以自动地为进程所使用(实现终端的操作)

  1. stdin 标准输入;
  2. stdout 标准输出;
  3. stderr 标准错误输出;
#include <stdio.h>
int main(int argc, char *argv[])
{
	int ch;
	while((ch = getchar()) != EOF){                  //缓存区中存数据的时候,遇见‘\n’或缓冲区存满才刷新缓存区 
		putchar(ch);                                 // 缓存区中取数据的时候也是一样 
	}
	return 0;
}
[root@192 learn]# ./a.out 
1
1
EOF
EOF
^C
#include <stdio.h>
int main(int argc, char *argv[])
{
	int ch;
	while((ch = fgetc(stdin)) != EOF){
		fputc(ch, stdout);
	}
	return 0;
}
[root@192 learn]# ./a.out 
1
1
2
2
^C

4. 缓冲区

  1. 标准IO缓冲区分为三种:全缓冲区、行缓冲区、不缓冲;

  2. 全缓冲区:当流指针和文件相关联的时候,此时访问的缓冲区为全缓冲区(当使用标准IO函数接口操作文件时,此时访问为全缓冲区),缓冲区的大小:4096 byte

    缓冲区的刷新:程序正常退出、缓冲区满、fflush()

  3. 行缓冲区:当流指针和终端相关联的时候,此时访问的缓冲区为行缓冲区(当使用标准IO函数接口操作终端时,此时访问的行缓冲区),stdin、stdout getchar putchar printf,缓冲区的大小:1024 byte,。

    缓冲区的刷新:程序正常退出,缓冲区满: fflush()\n

  4. 不缓冲: stderr;

二、文件IO

1. 文件IO

  1. 文件IO:直接系统调用;
  2. 标准IO的操作的核心:流指针;
  3. 文件IO的操作的核心:文件描述符,文件描述符就是内核当中 fd_array 数组的下标,是一个正整数。

0、1、2系统自动打开文件描述符(实现操作终端)

  1. 0 标准输入;
  2. 1 标准输出;
  3. 2 标准错误输出;

2.1. open函数原型

在这里插入图片描述

函数原型:

int open(const char *pathname, int flags);

int open(const char *pathname, int flags, mode_t mode);

  1. 功能:打开一个文件或者设备(硬件);

  2. 参数:pathname 路径名或文件名;flags 进程对该文件的执行权限;mode:该文件所属用户对文件的执行权限(如果第二个参数不指定O_CREAT,第三个参数忽略),默认:0664, 0755,mode & ~umask(位掩码0002)

    参数说明
    O_RDONLY以只读的方式打开文件
    O_WRONLY以只写的方式打开文件
    O_RDWR以读写的方式打开文件
    O_APPEND以追加的方式打开文件
    O_CREAT如果文件不存在,创建文件
    O_TRUNC如果文件已存在,清空文件中的数据

    对比:标准IO与文件IO对比。

    标准IO文件IO说明
    rO_RDONLY只读
    r+O_RDWR读写
    wO_WRONLY&O_CREAT&O_TRUNC只写,文件不存在会自动创建,文件存在则清空文件中数据
    w+O_RDWR|O_CREAT|O_TRUNC读写,文件不存在会自动创建,文件存在则清空文件中数据
    aO_WRONLY|O_CREAT|O_APPEND只写,文件不存在会自动创建,文件存在则追加内容
    a+O_WRONLY|O_CREAT|O_APPEND读写,文件不存在会自动创建,文件存在则追加内容
  3. 返回值:成功 大于等于0的整数(即文件描述符);失败 -1;

2.2. write函数原型

函数原型:ssize_t write(int fd, const void *buf, size_t count);

  1. 功能:向文件中写入count个字节的数据(根据数据的大小进行写入,不论数据的类型);终端输入字符串,追加写入文件中;

  2. 参数:

    fd:文件描述符;

    buf:需要写入文件的数据;

    count:需要写入的字节数;

  3. 返回值:成功 实际写入的字节数;失败 -1;

2.3. read函数原型

函数原型: ssize_t read(int fd, void *buf, size_t count);

  1. 功能:从文件中读取数据;

  2. 参数:

    fd:文件描述符;

    buf:保存读取的数据;

    count:期望读取的字节数;

  3. 返回值:成功 实际读取的字节数;失败 -1;0 (文件为空, 当前的读写位置处于文件的末尾);

2.4. lseek函数原型

函数原型: off_t lseek(int fd, off_t offset, int whence);

  1. 功能:读写位置定位;

  2. 参数:

    fd:文件描述符;

    offset:偏移量;

    whence:SEEK_SET、SEEK_CU、SEEK_END;

  3. 返回值:成功 返回定位的读写位置相对于文件开始处的偏移量;失败 返回 -1;

2.5. opendir函数原型

函数原型:DIR *opendir(const char *name);

  1. 功能:打开一个目录;

  2. 参数:

    name:目录名;

  3. 返回值:成功 目录流指针;失败 NULL;

2.6. readdir函数原型

函数原型:struct dirent *readdir(DIR *dirp);

  1. 功能:遍历指定目录路径下的所有文件;
  2. 参数:目录流指针;
  3. 返回值:失败 NULL;成功返回结构体(需定义结构体变量接收) ;

代码:

struct dirent {
    ino_t          d_ino;       /* inode number */
    off_t          d_off;       /* offset to the next dirent */
    unsigned short d_reclen;    /* length of this record */
    unsigned char  d_type;      /* type of file; not supported by all file system types */
    char           d_name[256]; /* filename */
};

2.7. stat函数原型

函数原型:int stat(const char *path, struct stat *buf);

  1. 功能:获取文件的属性信息;

  2. 参数:

    path:路径名或文件名;

    buf:保存文件的属性信息;

  3. 返回值:成功 0;失败 -1;

代码

struct stat {
        dev_t     st_dev;     /* ID of device containing file */  设备号
        ino_t     st_ino;     /* inode number */   索引号
        mode_t    st_mode;    /* protection */   用户的执行权限
        nlink_t   st_nlink;   /* number of hard links */  硬链接数
        uid_t     st_uid;     /* user ID of owner */  用户的ID
        gid_t     st_gid;     /* group ID of owner */  组ID
        dev_t     st_rdev;    /* device ID (if special file) */ 设备号
        off_t     st_size;    /* total size, in bytes */  文件的大小
        blksize_t st_blksize; /* blocksize for file system I/O */
        blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
        time_t    st_atime;   /* time of last access */ 上一次打开的时间
        time_t    st_mtime;   /* time of last modification */  上一次修改的时间
        time_t    st_ctime;   /* time of last status change */  上一次状态改变的时间
};

2. 库文件

  1. 静态库:在程序编译时,链接库文件,程序执行时,不需要库文件;.a后缀;
  2. 动态库(共享库): 在程序编译时,不需要链接库文件,在程序执行时,链接库文件;.so后缀;

库函数名及其源码如下:hello.c hello.h main.c

hello.c

#include "stdio.h"
int hello(void){
	printf("Hello Lib!");
	return 0;
}

hello.h

#ifndef __HELLO_H
#define __HELLO_H
int hello(void);
#endif

main.c

#include "hello.h"
int main(){
	hello();
}

2.1. 静态库的打包及使用

  1. hello.c编译为目标文件(即二进制文件);

    [root@192 source]# gcc -c hello.c -o hello.o
    
  2. .o文件打包成静态库,生成libhello.a文件;

    [root@192 source]# ar crs libhello.a hello.o
    
  3. 使用静态库,因为静态库是在编译的时候一起打包进程序的,所以如果编译的时候没有静态库文件,则无法编译,因为编译器找不到hello()的实现代码,如下所示:

    [root@192 source]# gcc main.c 
    /usr/bin/ld: /tmp/cccOFTDo.o: in function `main':
    main.c:(.text+0x5): undefined reference to `hello'
    collect2: error: ld returned 1 exit status
    

    所以,在编译的过程中要加入库引用:

    [root@192 source]# gcc -c main.c -L. -lhello -o a.out
    -l  指定库的名字,第三方库
    -L  指定库路径
    

    -L<路径>:引用自定义库的路径,如果调用系统库就不用-L.表示当前文件夹。

    -lxxxx:指定库的名字,第三方库,这里libhello.a只要写hello就可以。

2.2. 动态库的打包及使用

  1. hello.c编译为目标文件(即二进制文件),如果这里没有加-fpic下一步就会提示你重新用-fpic编译。

    [root@192 source]# gcc -c -fpic hello.c
    
  2. 将函数功能实现的代码,制作为动态库:-shared是生成动态库;-fpic生成位置无关代码,默认加。

    [root@192 source]# gcc -shared -fpic -o libhello.so hello.c
    
  3. 使用动态库:

    [root@192 source]# gcc -c main.c -L. -lhello -o a.out
    
  4. 动态库生效:

    1. 将动态库复制当前系统存放动态库的位置:

      [root@192 source]# cp libxxxx.so /usr/lib
      
    2. 配置环境变量(直接对全局变量进行赋值,指定库的路径):

      [root@192 source]# export LD_LIBRARY_PATH=<动态库所在的绝对路径>
      
    3. 修改配置脚本:

      /etc/ld.so.conf添加动态库所在路径;

      刷新配置文件:

      [root@192 source]# sudo ldconfig
      

三、进程

1. 进程简介

进程简介:

  1. 进程:进程是一个程序的一次执行的过程;

  2. 进程和程序的区别;

    程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;

    进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡;

  3. 创建:当程序执行(或者说进程创建时),系统会为每一个进程分配一个大小为4G的虚拟地址空间,用来存储进程所需的程序代码;使用变量,开辟内存的空间,生成task_struct(进程控制块)用来描述当前进程的状态信息;

  4. 调度:cpu调度,每一个进程都有属于自己的时间片,时间片用来设置当前进程从获取到CPU的执行权,到放弃执行权的时间

  5. 进程的上下文切换(上文:保存进程状态;下文:恢复进程上一次中断的状态);

  6. 退出:将进程使用的有限资源全部释放;

虚拟地址与物理地址:

  1. 虚拟地址空间(虚拟内存):虚设,本质实际不存在;
  2. 物理地址空间(物理内存):真实的,可以被CPU取址的内存区域;
  3. 虚拟地址空间,实际上是不存在,只是当前系统为每一个进程虚设的内存区域,内存区域的地址称之为虚拟地址;
  4. 如果进程需要属于自己虚拟地址空间,只需要对实际物理内存建立映射关系即可;
  5. 映射的本质:将物理地址转换为虚拟地址;
  6. 系统将虚拟地址空间分割为大小为4K内存单元(页单元),实际物理地址空间也被分割为大小为4K的内存单元;
  7. 虚拟地址和物理地址映射之后对应关系,由页映射表记录(页映射表由页表条目组成,每一个条目记录对应关系 0x200(物)-0x100(虚拟);

2. 进程的内存布局

  1. 程序文本段(程序段:Text):程序代码在内存中的映射,存放函数体的二进制代码。

  2. 数据段(已初始化数据段,未初始化数据段):存储的是当前进程使用的全局变量,静态变量;

    1. 初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。
    2. 未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。
  3. 堆区(Heap):存储动态内存分配,需要程序员手工分配,手工释放。注意它与数据结构中的堆是两回事,分配方式类似于链表。

  4. 栈区(Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。

3. 进程分类

  1. 进程:前台进程, 后台进程;
  2. 前台进程:可以与用户进行交互;
  3. 后台进程:不与用户进行交互,不受终端的控制;
  4. 守护进程(系统进程,属于后台进程的一种,系统开启时,创建该进程,系统退出,进程退出);
#include <stdio.h>
int main(int argc, const char *argv[])
{
	while(1){
		sleep(1);
		puts("zzzzzzzzzz");
	}
	return 0;
}

4. 进程状态切换

  1. 查看进程状态信息指令: ps axj aux

    PPID PID PGID SID TTY STAT UID TIME COMMAND

    父进程 进程ID 进程组ID 会话组ID 终端 进程名字

  2. 大部分的子进程都是父进程创建,少部分不是;创建子进程为了响应系统中的各种任务;

#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
	pid_t pid;
	int a = 3;
	pid = fork();
	if(pid < 0){
		perror("fork error");
		return -1;
	}
    else if(pid == 0){
		//child
		int a = 5;
		printf("In the child process, %d %p\n", a, &a);
	}
	else{
		//parent
		printf("In the parent process %d %p\n", a, &a);
	}
	return 0;
}
  1. D uninterruptible sleep (usually IO) 不可中断睡眠态
  2. R running or runnable (on run queue) 运行态
  3. S interruptible sleep (waiting for an event to complete) 可中断睡眠态
  4. T stopped, either by a job control signal or because it is being traced. 停止态(不运行,不释放资源)
  5. X dead (should never be seen) 死亡态(进程退出)
  6. Z defunct (“zombie”) process, terminated but not reaped by its parent.(僵尸态)
  7. For BSD formats and when the stat keyword is used, additional characters may be displayed:
  8. < high-priority (not nice to other users) 高优先级
  9. N low-priority (nice to other users) 低优先级
  10. L has pages locked into memory (for real-time and custom IO)
  11. s is a session leader 会话组组长
  12. l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do) 进程创建线程
  13. + is in the foreground process group. 前台执行

相关命令如下:

  1. ctrl z:将前台正在执行的命令放到后台,并且暂停;

  2. ctrl cctrl \:终止一个进程 ;

  3. bg:将进程放到后台运行 《====》 ./a.out &

  4. fg:将进程放到前台执行;

  5. top:动态查看(自动刷新);

  6. jobs:查看当前有多少在后台运行的命令;

  7. shift >:向下翻页;

    shift <:向上翻页;

  8. PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

  9. NI: -20 ~ 0;PR = NI + 20(NI值越小,优先级越高)

  10. 优先级 nice值

  11. renice:修改已经运行的进程的NICE:sudo renice -n -10(修改值) PID

  12. nice:对即将执行的进程修改NICE值:sudo nice -n -10 ./a.out

5. 子进程创建

5.1 getpid函数原型

函数原型:pid_t getpid(void);

  1. 功能:获取当前进程的 ID 号;
  2. 参数:无;
  3. 返回值:目前进程的进程 ID 号;

5.2 getppid函数原型

函数原型:pid_t getppid(void);

  1. 功能:获取当前进程父进程的ID号;
  2. 参数:无;
  3. 返回值:目前进程的父进程的ID号;

5.3 fork函数原型

函数原型:pid_t fork(void);,头文件:<unistd.h>

  1. 功能:创建新进程,通过复制调用进程生成,将新进程称为子进程,将调用进程称为父进程;
  2. 参数:无;
  3. 返回值:成功 父进程的代码中返回值为子进程的 ID,子进程的代码中返回值为 0。失败 父进程返回值 -1,子进程创建失败;

子进程通过复制父进程得来,子进程复制父进程的所使用的虚拟地址空间,子进程拥有和父进程相同的程序文本段,数据段,堆栈区;

  1. pid = fork();父进程运行 fork 开始创建子进程,在变量 pid 接受 fork 的返回值之前,子进程创建结束;
  2. 父进程的变量 pid 获取到子进程的 ID 号,表示子进程创建成功,子进程的变量 pid 获取到返回值是0;
  3. 父进程的变量 Pid 获取的值为 -1,表示子进程创建失败;
  4. 子进程不执行fork函数,并且不执行 fork 以上所有的执行代码;
  5. 子进程复制父进程使用缓冲区;
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
	pid_t pid = fork();
	if(pid < 0){
        // fork()创建子进程失败
		perror("fork error");
		return -1;
	}else if(pid == 0){
		//child
		printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
	}else if(pid > 0){
		//parent
		printf("In the parent process, child = %d parent = %d\n", pid, getpid());
	}
	return 0;
}

6. 孤儿进程

孤儿进程:父进程优先于子进程结束。

创建子进程成功,父进程退出,子进程不退出,子进程是孤儿进程,则init进程作为子进程的父进程;

子进程失去父进程之后,会认为1号进程(即init进程:负责初始化硬件,回收资源,回收和管理子进程)是自己的父进程。如果很多子进程都认为1号进程是自己的父进程,那么1号进程的负担会很大。所以,在编写代码时尽量让父进程回收完子进程的资源之后再结束。

孤儿进程是没有危害的。

#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
	pid_t pid = fork();
	if(pid < 0){
		perror("fork error");
		return -1;
	}else if(pid == 0){
		printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
		while(1);
	}else if(pid > 0){
		printf("In the parent process, child = %d parent = %d\n", pid, getpid());
	}
	return 0;
}

7. 僵尸进程

僵尸进程:子进程优先于父进程结束,但父进程没有回收子进程的资源。换句话说,子进程的任务已经完成了,但资源得不到回收,子进程就变成了僵尸进程。

  1. 僵尸态:进程退出,无法唤醒,进程资源不释放;
  2. 父进程退出,回收子进程的资源;
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc,const char *argv[])
{
	pid_t pid = fork();
	if (-1 == pid) {
		perror("fork");
		return -1;
	}else if(0 == pid) {
		//子进程结束,父进程未结束,资源没有被回收,变成僵尸进程
		exit(0);
	}else if(pid > 0) {
		//父进程
		while(1) {
			sleep(1);
		}
	}
	return 0;
}

僵尸进程是有危害的。回收僵尸进程的方法:

  1. 阻塞回收任意一个子进程资源 wait :父进程调用 wait 来回收子进程的资源,会阻塞等待回收,降低父进程的工作效率。

    函数原型:pid_t wait(int *status);

    1. 功能:阻塞等待子进程的状态发生改变,如果子进程状态未改变,函数阻塞,直到子进程状态改变,如果子进程状态是退出,wait函数回收子进程的资源;
    2. 参数:status 状态值;
      1. wait()会暂时停止目前进程的执行, 即阻塞父进程,等待子进程结束或者其他信号;
      2. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值;
      3. 子进程的结束状态值会由参数 status 返回, 而子进程的进程识别码也会一并返回.;
      4. 如果不考虑结束状态值, 则参数 status 可以设成 NULL;
    3. 返回值:成功 获取子进程的ID号;失败 -1;
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc,const char *argv[])
    {
    	pid_t pid = fork();
    	if (-1 == pid) {
    		perror("fork");
    		return -1;
    	}else if(0 == pid) {
    		exit(0);
    	}else if(pid > 0) {
    		sleep(6);		// 此时子进程为僵尸进程
            wait(NULL);		// 子进程资源被回收
    	}
    	return 0;
    }
    
  2. 非阻塞回收任意一个子进程资源 waitpid:父进程调用 waitpid 来回收子进程资源,需要频繁调用函数去查看子进程是否结束。

    函数原型:pid_t waitpid(pid_t pid, int *status, int options);

    1. 功能:回收子进程的资源;

    2. 参数:

      1. pid

        pid < -1:等待进程组 ID 等于 pid 的绝对值的进程组下的任一子进程;

        pid = -1:等待任何一个子进程退出,此时和wait()作用一样;

        pid = 0:等待与父进程同组的任何一个子进程;

        pid > 0:指定进程号执行等待;

      2. status:状态值同 wait;

      3. options

        0:同wait(),阻塞父进程,等待子进程退出;

        WNOHANG:非阻塞函数,不论子进程是否退出,立刻返回;

        WUNTRACED:若 pid 指定进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态 ;

    3. 返回值

      1. options 0:成功 子进程的ID号否则阻塞;失败 -1
      2. WNOHANG:成功 子进程退出,返回子进程的ID号;子进程不退出,返回0。失败 -1
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc,const char *argv[])
    {
    	pid_t pid = fork();
    	if (-1 == pid) {
    		perror("fork");
    		return -1;
    	}else if(0 == pid) {
    		exit(0);
    	}else if(pid > 0) {
            while(1){
                sleep(1);
                waitpid(-1, NULL, WNOHANG);
            }
    	}
    	return 0;
    }
    
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    int main(int argc, const char *argv[])
    {
    	pid_t pid = fork();112
        if(pid < 0){
    		perror("fork error");
    		return -1;
    	}else if(pid == 0){
    		sleep(10);
    		printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
    	}else{
    		pid_t ret;
    		while((ret = waitpid(-1, NULL, WNOHANG)) == 0){
    			sleep(1);
    		}
    		printf("ret = %d\n", ret);
    		printf("In the parent process, child = %d parent = %d\n", pid, getpid());
    		while(1);
    	}
    	return 0;
    }
    
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main(int argc, const char *argv[])
    {
    	pid_t pid = fork();
        if(pid < 0){
    		perror("fork error");
    		return -1;
    	}else if(pid == 0){
    		printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
    		while(1);
    	}else{
    		pid_t ret = waitpid(-1, NULL, WNOHANG);
    		printf("ret = %d\n", ret);
    		printf("In the parent process, child = %d parent = %d\n", pid, getpid());
    		while(1);
    	}
    	return 0;
    }
    
  3. 信号 signal:使用信号回收僵尸进程优于 wait 函数和 waitpid 函数。

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <signal.h>
    
    void myfun(int signum){
            pid_t pid = wait(NULL);
    }
    
    int main(int argc, const char *argv[])
    {
            if(SIG_ERR == signal(SIGCHLD, myfun)){
                    perror("signal");
                    return -1;
            }
            pid_t pid = fork();
            if(-1 == pid){
                    perror("fork error");
                    return -1;
            }else if(0 == pid){
                    sleep(6);
                    exit(0);        }else if(pid > 0){
                    while(1){
                            sleep(1);
                    }
            }
            return 0;
    }
    

8. 守护进程

8.1 定义及作用

守护进程(后台进程):由系统自动创建,系统开启,进程自动创建,系统关闭,进程自动退;响应任务,但可能不执行任务;

Linux 系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括:

  • 系统日志进程syslogd
  • web服务器httpd
  • 邮件服务器sendmail
  • 数据库服务器mysqld等。

8.2 创建流程

守护进程的创建:

  1. fork()创建子进程,父进程exit()退出。

    子进程成为孤儿进程,摆脱父进程,init进程成为其父进程。

    由于守护进程是脱离控制终端的,因此,完成第一步后就会在 Shell 终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

  2. 子进程中调用setsid()函数创建新的会话。

    在调用了fork()函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

  3. 再次fork()一个孙进程并让子进程退出。

    为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。

  4. 在孙进程中调用chdir()函数,让根目录"/"成为孙进程的工作目录(守护进程的默认工作目录是根目录)。

    这一步也是必要的步骤,使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如/mnt/usb)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,改变工作目录的常见函数是chdir

    1. int chdir(const char *path);
    2. 参数:路径名(改变目录) —— “/”;
  5. 在孙进程中调用umask()函数,设置进程的文件权限掩码为 0。

    umask(mode_t mask);参数:修改的值;

    文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)

  6. 在孙进程中关闭任何不需要的文件描述符。

    同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

    在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

    int getdtablesize(void);

    功能:获取最大文件描述符

    for(fd = 0; fd < getdtablesize;fd++){
        close(fd);
    }
    
  7. 守护进程退出程序。

    当用户需要外部停止守护进程运行时,往往会使用kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。

实例:

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

void creat_daemon();

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

    // 创建守护进程
    creat_daemon();

    return 0;
}

void creat_daemon(){
    // 创建子进程并退出父进程;
    pid_t pid = fork();
    if (pid == -1)
    {
        printf("fork error!\n");
        exit(1);
    }else if (pid)
    {
        exit(0);
    }
    
    // 在子进程中创建新的会话
    if (-1 == setsid())
    {
        printf("setsid error!\n");
        exit(1);
    }
    
    // 在子进程中创建孙进程
    pid = fork();
    if (pid == -1)
    {
        printf("fork error!\n");
        exit(1);
    }else if (pid)
    {
        // 退出孙进程的父进程
        exit(0);
    }
    
    // 改变孙进程的工作目录
    chdir("/");
    
    // 设置进程的文件权限掩码
    umask(0);

    // 关闭不需要的文件描述符
    for (int i = 0; i < 3; i++)
    {
        close(i);
    }
    
    return;
}

9. exec函数族

我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

函数族:exec函数族分别是execlexeclpexecleexecvexecvpexecvpe

函数原型:

#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[]);
  1. 功能:运行新进程,替换到调用进程,并且新进程占有调用进程的地址空间;

  2. 参数:

    path:可执行文件的路径名;

    file:如果参数 file 中包含/,则将其视为路径名,否则按PATH环境变量,在它所指的各目录中搜索可执行文件。

    arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且 arg 必须以 NULL结束;

  3. 返回值:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

使用实例见:(115条消息) linux进程—exec族函数(execl, execlp, execle, execv, execvp, execvpe)_云英的博客-CSDN博客

四、线程

1. 线程的定义

线程:进程中的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。线程在运行中呈现间断性。

线程由进程创建,创建的线程共享该进程的地址空间(线程与线程之间,共享进程的数据段、程序文本段、堆区, 每一个线程享有自己独立的栈区,保存自己使用的局部变量,参数,返回值),在同一个进程中创建的线程共享该进程的地址空间;;

进程的两个基本属性:

  1. 进程是一个可拥有资源的独立单位;
  2. 进程是一个可以独立调度和分派的基本单位。

多线程与子进程:

  1. 子进程拥有自己独立的地址空间,如果实现进程间数据的交互,需要引入进程间通信机制;

    线程与线程之间通信,不需要引入通信机制,线程间是共享进程的数据段(全局变量,静态变量),因此线程间需要进行通信直接操作共享进程的数据段;

    将并发执行的任务(线程)访问同一个数据,称之为竞态;

    线程间进行通信,需要同步互斥机制,保证任何一个时刻,只有一个任务(线程)在访问共享数据(保证数据的原子性)(任何一个时刻,只有一个线程进入自己的临界区);

    临界区:将任务(线程)访问共享数据的操作代码;

  2. 多线程可以相应多个任务,相应多任务,也可以通过创建子进程来完成,但是频繁创建子进程带来系统消耗;

    创建子进程需要父进程的所有属性,但是创建线程不需要复制,线程共享进程的属性;

2. 线程的创建与退出

函数原型:线程创建。

#include <pthread.h>

int pthread_create(pthread_t *restrict thread,
                   const pthread_attr_t *restrict attr,
                   void *(*start_routine)(void *),
                   void *restrict arg);
  1. 功能:在进程中创建新线程;

  2. 参数:

    thread:指向线程标识符的指针;

    attr:设置线程的属性,默认为NULL

    start_routine:线程运行函数起始地址;

    arg:运行函数的参数,它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。;

  3. 返回值:成功 0;失败 错误码;

函数原型:线程退出。

#include <pthread.h>

noreturn void pthread_exit(void *retval);
  1. 功能:终止调用线程;通常情况下,pthread_exit()函数是在线程完成工作后无需继续存在时被调用。如果main()是在它所创建的线程之前结束,并通过pthread_exit()退出,那么其他线程将继续执行。否则,它们将在main()结束时自动被终止。
  2. 参数:retval 退出时,设置的状态值;
  3. 返回值:无返回值;

代码实例:

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

#define NUMBER_OF_THREADS 10

void *PrintHelloWorld(void * tid);

int main()
{
    // 定义线程描述符,多个文件描述符可用数组
    pthread_t thread[NUMBER_OF_THREADS];
    
    for (int i = 0; i < NUMBER_OF_THREADS; i++)
    {
        printf("Main here. Creating thread %d\n", i);

        // 创建线程,传参分别为(指向线程标识符的指针,线程属性NULL,线程运行的打印函数,线程运行函数参数)
        int status = pthread_create(&thread[i], NULL, PrintHelloWorld, (void *)i);
        if (status)
        {
            printf("pthread_create return error code %d!\n", status);
            exit(-1);
        }
    }
    
    // 退出线程
    pthread_exit(NULL);
    return 0;
}

// 线程运行函数
void *PrintHelloWorld(void * tid)
{
    printf("Hello World %d.\n", tid);
    pthread_exit(0);
}
Main here. Creating thread 0
Main here. Creating thread 1
Hello World 0.
Main here. Creating thread 2
Hello World 1.
Hello World 2.
Main here. Creating thread 3
Main here. Creating thread 4
Hello World 3.
Main here. Creating thread 5
Hello World 4.
Main here. Creating thread 6
Hello World 5.
Main here. Creating thread 7
Hello World 6.
Main here. Creating thread 8
Hello World 7.
Main here. Creating thread 9
Hello World 8.
Hello World 9.

由于没有在主线程中等待我们创建出来的10个线程执行完毕,所以创建出来的子线程可能还没来得及执行完成,就因为主线程(main函数)执行完毕而终止整个进程,导致子线程没法运行。因此printf得到的Hello world不是10个,其数量是无法预知的,其顺序也是无法预知的。

3. 阻塞等待线程回收资源

函数原型:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  1. 功能:

    1. 阻塞等待其他线程结束:当调用pthread_join()时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
    2. 对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用pthread_join()的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
  2. 参数:

    thread:线程标识符,即线程 ID,标识唯一线程(用户自定义);

    retval:用户定义的指针,用来存储被等待线程的返回值;

  3. 返回值:成功 0;失败 错误码;

代码实例:

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

#define NUMBER_OF_THREADS 10

void *PrintHelloWorld(void *tid);

int main()
{
    pthread_t thread[NUMBER_OF_THREADS];
    
    for (int i = 0; i < NUMBER_OF_THREADS; i++)
    {
        printf("Main here. Creating thread %d\n", i);

        int status = pthread_create(&thread[i], NULL, PrintHelloWorld, (void *)i);
        if (status)
        {
            printf("pthread_create return error code %d!\n", status);
            exit(-1);
        }
        
        // 阻塞等待进程结束回收资源
        pthread_join(thread[i], NULL);
    }
        
    exit(0);
}

void *PrintHelloWorld(void *tid)
{
    printf("Hello World %d.\n", tid);
    pthread_exit(0);
}
Main here. Creating thread 0
Hello World 0.
Main here. Creating thread 1
Hello World 1.
Main here. Creating thread 2
Hello World 2.
Main here. Creating thread 3
Hello World 3.
Main here. Creating thread 4
Hello World 4.
Main here. Creating thread 5
Hello World 5.
Main here. Creating thread 6
Hello World 6.
Main here. Creating thread 7
Hello World 7.
Main here. Creating thread 8
Hello World 8.
Main here. Creating thread 9
Hello World 9.

4. 取消线程

函数原型:

/* Cancel THREAD immediately or at the next possibility.  */
extern int pthread_cancel (pthread_t __th);
  1. 功能:向线程发送一个取消请求,将线程退出;

  2. 参数:thread 线程标识符;

  3. 返回值:成功 0;失败 错误码;

函数原型:

/* Test for pending cancellation for the current thread and terminate
   the thread as per pthread_exit(PTHREAD_CANCELED) if it has been
   cancelled.  */
extern void pthread_testcancel (void);
  1. 功能:测试当前线程是否取消,如果线程已被取消,则按照pthread_exit(PTHREAD_CANCELED)终止线程;

  2. 参数:无;

  3. 返回值:无;

代码实例:

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

void *child(void *arg){
    int i = 0;
    while (1)
    {
        printf("child running: %d\n", i++);
        pthread_testcancel();
    }
}

int main(){
    pthread_t pid;
    pthread_create(&pid, NULL, child, NULL);
    sleep(1);
    pthread_cancel(pid);
    pthread_join(pid, NULL);
}

5. 线程的同步互斥机制

当多线程在访问全局数据段,实现通信,产生竞态,引入同步互斥机制,保证任何一个时刻只有一个线程在访问共享数据段。

5.1 锁

在这里插入图片描述

多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。

最常用的就是互斥锁,当然还有很多种不同的锁,比如自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。

如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。

所以,为了选择合适的锁,我们不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率。

对症下药,才能减少锁对高并发性能的影响。

5.1.1 互斥锁

互斥锁代表是一种锁资源,互斥锁的工作原理:保证对共享资源操作的原子性(完整性);

当多线程在对同一个资源进行访问,在访问之前,执行申请锁操作,如果某一个线程获取到该锁,开始执行访问数据;如果线程在申请锁时,锁资源已经被占用,该线程就会执行等待,直到锁资源被释放;

互斥锁不能保证线程(任务)的执行先后;

当访问共享数据的任务采用了互斥锁,其他任务在访问同一个数据时,必须采用互斥锁机制,如果某一个任务不执行锁操作,锁资源无效;

初始化锁

  1. 静态创建:用宏PTHREAD_MUTEX_INITIALIZER来静态的初始化锁,采用这种方式比较容易理解,互斥锁是pthread_mutex_t的结构体,而这个宏是一个结构常量。静态初始化锁函数原型:

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
  2. 动态创建函数原型:

    /* Initialize a mutex.  */
    extern int pthread_mutex_init (pthread_mutex_t *__mutex,
                                   const pthread_mutexattr_t *__mutexattr)
    
    1. 功能:初始化互斥锁;

    2. 参数:

      mutex:互斥锁的标识符;

      attr:互斥锁的属性;

    3. 返回值:成功 0;失败 错误码;

销毁锁/释放锁

函数原型:

/* Destroy a mutex.  */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
  1. 功能:释放锁资源;

  2. 参数:

    mutex:互斥锁的标识符;

  3. 返回值:成功 0;失败 错误码;

尝试加锁/申请互斥锁

函数原型:

/* Try locking a mutex.  */
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
  1. 功能:执行上锁操作(非阻塞);

  2. 参数:

    mutex:互斥锁的标识符;

  3. 返回值:成功 0;失败 错误码;

加锁/申请互斥锁

函数原型:

/* Lock a mutex.  */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
  1. 功能:执行上锁操作(阻塞);

  2. 参数:

    mutex:互斥锁的标识符;

  3. 返回值:成功 0;失败 错误码;

解锁

函数原型:

/* Unlock a mutex.  */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
  1. 功能:执行解锁操作;

  2. 参数:mutex 互斥锁的标识符;

  3. 返回值:成功 0;失败 错误码;

互斥锁死锁理解不深

产生死锁的方式:

  1. 互斥锁交叉嵌套;
  2. 同一个互斥锁嵌套使用;
  3. 占有所资源的任务异常退出,锁资源未释放;

学习:(115条消息) Linux环境下,C语言预防死锁的方法、产生死锁的实际情况(参考APUE的11章以及12章)_c语言死锁_ySh_ppp的博客-CSDN博客

5.1.2 读写锁

为了解决性能问题,引入读写锁,特点是:读共享,写独享,写优先级高,读写之间也是互斥的。适合读线程较多的场景。
🌴🌴 注意:在A线程加锁状态下,当B线程请求读锁,然后C线程请求写锁时,在A释放锁的情况下,C请求的写锁先获取到。

函数原型:

//初始化
pthread_rwlock_t mutex = PTHREAD_RWLOCK_INITIALIZER;
//或者
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
		const pthread_rwlockattr_t * _Nullable __restrict);

//加锁(读写)
int pthread_rwlock_rdlock(pthread_rwlock_t *);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *)

int pthread_rwlock_wrlock(pthread_rwlock_t *);
int pthread_rwlock_trywrlock(pthread_rwlock_t *)//解锁
pthread_rwlock_unlock()//释放锁
int pthread_rwlock_destroy(pthread_rwlock_t * )

5.2. 信号量

使用信号量可以实现协同确定任务的执行先后,保证任何一个时刻只有一个任务进入临界区;

信号量的工作原理:信号量声明信号量的值;

对于访问共享资源,都需要先访问信号量的值:申请信号量:P操作 -1;释放信号量:V操作 +1

当执行任务申请信号量时,如果当前信号量的值大于0,申请成功;反之信号量的值为 -1;如果此时信号量的值为0,申请就会阻塞,直到其他任务释放信号量;

  1. 初始化信号量的值,函数原型:

    #include <semaphore.h>
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    
    1. 功能:初始化信号量;

    2. 参数:

      sem:信号量的标识符;

      pashred:0 用于线程间,非0 用于进程间;

      value:信号量的初始值;

    3. 返回值:成功 0;失败 -1;

  2. 申请信号量(加锁),函数原型:int sem_wait(sem_t *sem);

    #include <semaphore.h>
    int sem_wait(sem_t *sem);			// 信号量 < 0,函数阻塞等待(阻塞)
    int sem_trywait(sem_t *sem);		// 非阻塞
    int sem_timedwait(sem_t *restrict sem,
                      const struct timespec *restrict abs_timeout);
    
    1. 功能:申请信号量,信号量的值 -1;

    2. 参数:

      sem:信号量的标识符;

    3. 返回值:成功 0;失败 -1;

  3. 释放信号量(解锁),函数原型:

    #include <semaphore.h>
    int sem_post(sem_t *sem);
    
    1. 功能:释放信号量信号量的值 +1;

    2. 参数:

      sem:信号量的标识符;

    3. 返回值:成功 0;失败 -1;

  4. 摧毁信号量,函数原型:

    #include <semaphore.h>
    int sem_destroy(sem_t *sem);
    
    1. 功能:销毁信号量;

    2. 参数:

      sem:信号量的标识符;

    3. 返回值:成功 0;失败 -1;

  5. 获取当前信号量的值,函数原型:

    #include <semaphore.h>
    int sem_getvalue(sem_t *restrict sem, int *restrict sval);
    
    1. 功能:获取当前信号量的值;

    2. 参数:

      sem:信号量的标识符;

      sval:保存当前信号量的值;

    3. 返回值:成功 0;失败 -1;

代码实例:

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

#define NUMBER_OF_THREADS 2
// 定义信号量标识符
sem_t sem;

void *func1(void *arg){
	// 申请信号量,信号量值 -1
	sem_wait(&sem);
	int *running=arg;
	printf("pthread2 running\n");
	printf("%d\n",*running);
}

void *func2(void *arg){
	// 释放信号量,信号量 +1
	sem_post(&sem);
	printf("pthread2 running\n");
}

void pthread_log(int status){
	if(status){
		printf("pthread_create return error code!\n", status);
		exit(-1);
	}
}

int main(){
	// 初始化信号量,线程间通信且设置信号量初始值为0;
	sem_init(&sem, 0, 0);
	// 定义线程描述符2个
	pthread_t thread[NUMBER_OF_THREADS];
	int status, a = 5, i = 0, value = 0;
	// 创建线程1
	status = pthread_create(&thread[0], NULL, func1, (void *)&a);
	pthread_log(status);
	printf("main thread1 running!\n");
	sleep(5);
	// 创建线程2
	status = pthread_create(&thread[1], NULL, func2, (void *)&a);
	pthread_log(status);
	printf("main thread2 running!\n");
	// 获取当前信号量的值
	sem_getvalue(&sem, &value);
	printf("sem = %d\n", value);
	// 回收资源
	pthread_join(thread[0], NULL);
	pthread_join(thread[1], NULL);
	// 销毁信号量集
	sem_destroy(&sem);
	return 0;
}

5.3. 条件变量

条件变量工作原理:对当前不访问共享资源的任务,直接执行睡眠处理,如果此时需要某个任务访问资源,直接将该任务唤醒 ;

  • 条件变量类似异步通信,操作的核心:睡眠、唤醒;
  • 条件变量:如果唤醒操作发生在睡眠之前,唤醒操作无效,睡眠处理一定发生在唤醒之前;

条件变量本质不是锁,通常与互斥锁配合使用。当条件不满足时,阻塞线程。当条件满足时,通知线程解除阻塞。

  1. 初始化条件变量:

    1. 方法一:静态初始化条件变量;

      pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

    2. 方法二:动态初始化条件变量,函数原型:

      #include <pthread.h>
      
      int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
      
      1. 功能:初始化条件变量;

      2. 参数:

        cond:条件变量标识符;

        addr:属性;

      3. 返回值:成功 0,失败 错误码;

  2. 条件不足阻塞,函数原型:

    #include <pthread.h>
    
    int pthread_cond_wait(pthread_cond_t *restrict cond,
                          pthread_mutex_t *restrict mutex);
    int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                               pthread_mutex_t *restrict mutex,
                               const struct timespec *restrict abstime);
    
    1. 功能:主动挂起等待信号的到来;

      每个条件变量总是有一个互斥锁与之关联,当pthread_cond_wait运行后,会原子地执行2个动作;

      1. 给互斥锁mptr解锁,让出这把锁的使用权,把调用线程投入睡眠;
      2. 直到另外某个线程就本条件变量调用pthread_cond_signalpthread_cond_wait返回前重新给mptr上锁;
    2. 参数:

      cond:条件变量标识符;

      mutex:互斥锁标识符;

    3. 返回值:成功 0,失败 错误码;

  3. 条件满足则唤醒,函数原型:

    #include <pthread.h>
    
    int pthread_cond_signal(pthread_cond_t *cond);
    
    1. 功能:发送一个条件变量成立的信号,解除阻塞,执行唤醒操作;

    2. 参数:cond 条件变量标识符;

    3. 返回值:成功 0,失败 错误码;

  4. 释放条件变量,函数原型:

    #include <pthread.h>
    
    int pthread_cond_destroy(pthread_cond_t *cond);
    
    1. 功能:摧毁条件变量;

    2. 参数:cond 条件变量标识符;

    3. 返回值:成功 0,失败 错误码;

代码实例:

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

#define N 128
char buf[N] = "";
pthread_mutex_t lock;
pthread_cond_t cond;

void *thread_handler1(void *arg){
	while(1){
		// 从终端向buf存入N个字节
		fgets(buf, N, stdin);
		buf[strlen(buf) - 1] = '\0';
		// 唤醒cond
		pthread_cond_signal(&cond);
	}
	// 线程退出
	pthread_exit(0);
}

void *thread_handler2(void *arg){
	while(1){
		sleep(3);
		printf("thread2 running!\n");
		// 加锁并挂起等待信号
		pthread_mutex_lock(&lock);
		pthread_cond_wait(&cond, &lock);
		printf("buf = %s\n", buf);
		// 解锁
		pthread_mutex_unlock(&lock);
	}
	pthread_exit(0);
}

int main(int argc, const char *argv[]){
	pthread_t thread1, thread2;
	// 初始化互斥锁和条件变量
	pthread_mutex_init(&lock, NULL);
	pthread_cond_init(&cond, NULL);
	// 创建线程
	if(pthread_create(&thread1, NULL, thread_handler1, NULL) != 0){
		perror("pthread_create error!");
		exit(-1);
	}
	if(pthread_create(&thread2, NULL, thread_handler2, NULL) != 0){
		perror("pthread_create error!");
		exit(-1);
	}
	// 阻塞等待回收资源
	pthread_join(thread1, NULL);
	pthread_join(thread2, NULL);
	// 释放锁
	pthread_mutex_destroy(&lock);
	// 释放条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

五、通信机制

1. 进程的通信机制

进程通信地六大机制
管道,也称共享文件
有名管道:fifo
无名管道:pipe
信号:signal
System V IPC对象
共享内存:share memory,也称共享存储
消息队列:message queue,也称消息传递
信号量和PV操作:semaphore
套接字:socket
本地通信
网络通信

2. 无名管道

无名管道:在进程访问的共有的内核空间上创建一个特殊的文件,称为管道;

无名管道属性:

  1. 只能用于具有亲缘关系的进程之间的通信,也就是说,匿名管道只能用于父子进程之间的通信
  2. 半双工的通信模式,具有固定的读端和写端;
  3. 管道可以看成是一种特殊的文件,对于它的读写可以使用文件 IO 如 read、write 函数;
  4. 无名管道的操作属于一次性操作,如果对无名管道执行读操作,数据就会被读走;
  5. 无名管道管道的大小是固定的,管道一旦写满,写操作就会阻塞,管道的大小为64K
  6. 当管道中无数据,执行读操作,读操作阻塞;
  7. 无名管道写满,写操作阻塞,如果管道中有大于4K的空间,写操作可以继续,每次最多写入4K的整倍数;
  8. 无名管道不保证操作的原子性,如果当前管道,满足读写条件,读写可以并发;
  9. 向无名管道中写数据,将读端关闭,管道损坏,进程收到信号(SIGPIPE),将进程退出;
  10. 当管道中有数据,将写端关闭,读操作可以执行,之后数据读完,可以继续读取(非阻塞);

创建无名管道,函数原型:

#include <unistd.h>

int pipe(int pipefd[2]);
  1. 功能:创建无名管道;

  2. 参数: 一个存储空间为 2 的文件描述符数组,

    pipefd[0]:管道的读端;

    pipefd[1]:管道的写端;

    pipefd[2] :保存的是操作管道的两个文件描述符;

  3. 返回值:成功 0,失败 -1;

实例:实现一个进程向另一个进程传输文件内容

代码实例:

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


#define Size 128

int main(int argc, const char *argv[]){
	int fd[2];
	char buf[Size] = {0};
	// 建立管道
    if (pipe(fd) != 0){
	    printf("create pipe failed!\n");
	    return -1;
    }
    int pid = fork();
    if (pid < 0){
	    printf("fork failed!\n");
	    return -1;
    }else if (pid == 0){
	    printf("child!\n");
        // 关闭子进程写端,操作读端,从管道内读取128字节数据
	    close(fd[1]);
	    read(fd[0], buf, 128);
	    printf("child printf message from parent: %s!\n", buf);
    }else{
	    printf("parent!\n");
	    sleep(3);
        // 关闭父进程的读端,操作写端,向管道写入"hello from parent"
	    close(fd[0]);
	    write(fd[1], "hello from parent", strlen("hello from parent"));
    }
    return 0;
}

无名管道缺点:无名管道由于没有名字,只能用于父子进程间的通信。为了克服这个缺点,提出了有名管道;

3. 有名管道

有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见;

进程通过文件IO来操作有名管道;遵循先进先出规则;不支持如lseek()操作 ;

创建有名管道,函数原型:

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  1. 功能:创建有名管道;

  2. 参数:

    pathname:路径名(管道名);

    mode:文件所属用户的执行权限;

  3. 返回值:成功 0,失败 -1;

代码实例:

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

#define N 128

int main(int argc, const char *argv[]){
	int fd;
	char buf[N] = "";
	if(mkfifo("fifo", 0775) != 0){
		if(errno == EEXIST){
			fd = open("fifo", O_RDWR);
			printf("fifo exit,fd = %d\n", fd);
		}else{
			perror("mkfifo error!");
			return -1;
		}
	}else{
		fd = open("fifo", O_RDWR);
		printf("fd = %d\n", fd);
	}
	while(1){
		read(fd, buf, N);
		printf("buf:%s\n", buf);
	}
	return 0;
}

有名管道缺点:管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。

4. system V IPC

System V引入了三种高级进程间的通信机制:消息队列、共享内存和信号灯(信号量集)。

4.1 消息队列

消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。

消息队列中消息本身由消息类型和消息数据组成,通常使用如下结构:

struct msgbuf {
    long mtype;       /* message type, must be > 0 */   消息的类型(选择性的读取)
    char mtext[1];    /* message data */   消息的正文(数组,变量,结构体)
 };

消息队列工作原理:在内核空间上创建队列,信息发送者将发送信息打包成节点添加到队列中,信息的接受者选择性从队列上读取想要的节点;

  1. ipcs -q:查看系统中使用消息队列的情况;
  2. ipcrm -q + msqid:删除消息队列;

消息队列:创建队列,向队列中添加信息,从队列移除信息,实现队列的控制(获取队列的属性,设置队列的属性,删除不使用队列);

  1. 生成 key 值(键值),确保消息队列的唯一性,函数原型:

    #include <sys/ipc.h>
    
    key_t ftok(const char *pathname, int proj_id);
    
    1. 功能:生成 key 值;

    2. 参数:

      pathname:路径名,用户给定,必须真实存在;

      proj_id:传字符;可以根据自己的约定,随意设置。这个数字,有的称之为 project ID; 在UNIX 系统上,它的取值是 1 到 255;

    3. 返回值:成功 key值 随机数;失败 -1;

  2. 创建消息队列,函数原型:

    #include <sys/msg.h>
    
    int msgget(key_t key, int msgflg);
    
    1. 功能:创建消息队列,或访问一个已经存在的消息队列。;

    2. 参数:

      key值:0或非0,key值确保消息队列的唯一性;

      msgflg:标志位。IPC_CREAT|IPC_EXCL|0664:创建并打开消息队列(如果消息队列不存在,自动创建,如果已存在,返回 EEXIST)访问权限为0664;

    3. 返回值:成功 消息队列标识符;失败 -1;

  3. 消息添加到消息队列末尾,函数原型:

    #include <sys/msg.h>
    
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    
    1. 功能:将消息添加到队列的末尾;

    2. 参数:

      msqid:消息队列的标识符;

      msgp:发送的消息;

      msgsz:消息正文的大小sizeof(struct msgbuf) - sizeof(long)

      msgflg 0:消息队列没空间,写操作阻塞;

    3. 返回值:成功 0;失败 -1;

  4. 将消息从队列中移除,函数原型:

    #include <sys/msg.h>
    
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
                   int msgflg);
    
    1. 功能:将消息从队列中移除;

    2. 参数:

      msqid:消息队列标识符;

      msgp:保存接受的消息(定义和写入时保持一致);

      msgsz:消息正文的大小;

      msgtyp:消息的类型

      1. =0:读取消息队列中的第一个消息;
      2. >0:读取消息队列中类型为 msgtype 的第一个消息;
      3. <0:读取消息队列中不小于 msgtyp 的绝对值,且类型最小的第一个消息;

      msgflg 0:消息队列中无数据,读操作阻塞;

    3. 返回值:成功 消息正文的大小;失败 -1;

  5. 对消息队列的控制,函数原型:

    #include <sys/msg.h>
    
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    1. 功能:实现对消息队列的控制;

    2. 参数:

      msqid:消息队列的标识符;

      cmd:指定消息队列的操作处理;IPC_STAT 从第三个参数读取消息队列的属性信息,IPC_SET 设置消息队列的属性;IPC_RMID 删除消息队列;

      buf:描述消息队列属性的结构体;

      struct msqid_ds {
          struct ipc_perm msg_perm;     /* Ownership and permissions */ 消息队列的权限
          time_t          msg_stime;    /* Time of last msgsnd(2) */    消息队列发送消息的时间
          time_t          msg_rtime;    /* Time of last msgrcv(2) */    消息队列接受消息的时间
          time_t          msg_ctime;    /* Time of last change */      消息队列改变的时间
          unsigned long__msg_cbytes; /* Current number of bytes in queue (nonstandard) */消息队列当前的字节数
          msgqnum_t msg_qnum;/* Current number of messages in queue */消息队列当前消息的个数
          msglen_t msg_qbytes;/* Maximum number of bytes allowed in queue */消息队列最大的字节数
          pid_t           msg_lspid;    /* PID of last msgsnd(2) */   发送消息的用户ID
          pid_t           msg_lrpid;    /* PID of last msgrcv(2) */   接收消息的用户ID
      }; 
      
    3. 返回值:返回值:成功 0;失败 -1;

代码实例:实现两个终端数据交互;

  1. 终端1:

    #include <stdio.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/types.h>
    #include <errno.h>
    #include <signal.h>
    
    #define N 128
    #define TYPE1 100
    #define TYPE2 200
    // 消息正文的大小
    #define SIZE (sizeof(struct msgbuf) - sizeof(long))
    
    // 定义消息队列结构体
    struct msgbuf{
    	long mtype;
    	char mtext[N];
    };
    
    int main(int argc, const char *argv[]){
    	struct msgbuf msg_snd, msg_rcv;
    	key_t key;
    	// 生成key值
    	if((key = ftok(".", 'a'))  == -1){
    		perror("ftok error!");
    		return -1;
    	}
    	int msgid;
    	// 创建或打开消息队列,获取消息队列标识符
    	if((msgid = msgget(key, IPC_CREAT|IPC_EXCL|0664))  == -1){
    		if(errno != EEXIST){
    			perror("msgget error!");
    			return -1;
    		}else{
    			msgid = msgget(key, 0664);
    		}
    	}
    	// 创建子进程
    	pid_t pid = fork();
    	if(pid < 0){
    		perror("fork error!");
    		return -1;
    	}else if(pid == 0){
    		while(1){
    			// 子进程发送消息
    			msg_snd.mtype = TYPE1;
    			fgets(msg_snd.mtext, N, stdin);
    			msg_snd.mtext[strlen(msg_snd.mtext) - 1] = '\0';
    			if(strncmp(msg_snd.mtext, "quit", 4) == 0){
    				kill(getppid(), SIGKILL);
    				break;
    			}
                // 将消息添加到消息队列末尾
    			msgsnd(msgid, &msg_snd, SIZE, 0);
    		}
    	}else{
    		while(1){
    			// 父进程接收消息
    			msgrcv(msgid, &msg_rcv, SIZE, TYPE2, 0);
    			if(strncmp(msg_rcv.mtext, "quit", 4) == 0){
    				kill(pid, SIGKILL);
    				goto err;
    			}
    			printf("msg_tty2:%s\n", msg_rcv.mtext);
    		}
    	}
    	return 0;
    err:
    	msgctl(msgid, IPC_RMID, NULL);
    }
    
  2. 终端2:

    #include <stdio.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/types.h>
    #include <errno.h>
    #include <signal.h>
    #include <string.h>
    
    #define N 128
    #define TYPE1 100
    #define TYPE2 200
    #define SIZE (sizeof(struct msgbuf) - sizeof(long))
    
    struct msgbuf{
    	long mtype;
    	char mtext[N];
    };
    
    int main(int argc, const char *argv[]){
    	struct msgbuf msg_snd, msg_rcv;
    	key_t key;
    	// 生成key值
    	if((key = ftok(".", 'a'))  == -1){
    		perror("ftok error!");
    		return -1;
    	}
    	int msgid;
    	// 创建或打开消息队列
    	if((msgid = msgget(key, IPC_CREAT|IPC_EXCL|0664))  == -1){
    		if(errno != EEXIST){
    			perror("msgget error!");
    			return -1;
    		}else{
    			msgid = msgget(key, 0664);
    		}
    	}
    	// 创建子进程
    	pid_t pid = fork();
    	if(pid < 0){
    		perror("fork error!");
    		return -1;
    	}else if(pid == 0){
    		while(1){
    			// 子进程发送消息
    			msg_snd.mtype = TYPE2;
    			fgets(msg_snd.mtext, N, stdin);
    			msg_snd.mtext[strlen(msg_snd.mtext) - 1] = '\0';
    			if(strncmp(msg_snd.mtext, "quit", 4) == 0){
    				kill(getppid(), SIGKILL);
    				break;
    			}
                // 将消息添加到消息队列末尾
    			msgsnd(msgid, &msg_snd, SIZE, 0);
    		}
    	}else{
    		while(1){
    			// 父进程接收消息
    			msgrcv(msgid, &msg_rcv, SIZE, TYPE1, 0);
    			if(strncmp(msg_rcv.mtext, "quit", 4) == 0){
    				kill(pid, SIGKILL);
    				goto err;
    			}
    			printf("msg_tty1:%s\n", msg_rcv.mtext);
    		}
    	}
    	return 0;
    err:
    	msgctl(msgid, IPC_RMID, NULL);
    }
    

4.2 共享内存

工作原理:系统为每一个进程创建4G的虚拟地址空间,共享内存开辟一块实际的物理内存区域(共享内存区域),将该物理内存映射给每一个进程;

本质:多进程实际都是在访问同一块物理内存区域;

通信效率最高,适用场合:实现实时数据传输;

多进程访问同一块物理内存区域

容易产生竞态,共享内存结合同步互斥机制,保证数据的正确性,任何一个时刻只有一个任务在访问共享的内存区域;

  1. 共享内存的创建,函数原型:

    #include <sys/shm.h>
    
    int shmget(key_t key, size_t size, int shmflg);
    
    功能开辟实际物理内存区域;
    参数key标识共享内存的键值: 0/IPC_PRIVATE。通常此值来源于 ftok 返回的IPC键值;
    size物理内存大小,以字节为单位;
    shmflgIPC_CREAT|IPC_EXCL|0664:创建并打开物理内存(内存不存在,开辟;已存在,报错),0664 权限打开物理内存;
    返回值成功返回共享内存的标识符;
    失败返回 -1,错误原因存于error中;
    错误代码EINVAL参数size小于SHMMIN或大于SHMMAX;
    EEXIST预建立 key 所指的共享内存,但已经存在;
    EIDRM参数 key 所指的共享内存已经删除;
    ENOSPC超过了系统允许建立的共享内存的最大值 (SHMALL);
    ENOENT参数 key 所指的共享内存不存在,而参数 shmflg 未设 IPC_CREAT 位;
    EACCES没有权限;
    ENOMEM核心内存不足;

    在 Linux 环境中,对开始申请的共享内存空间进行了初始化,初始值为0x00

  2. 将物理内存映射到进程的虚拟地址空间(进程只需要操作属于自己的虚拟地址空间,本质访问实际物理内存),函数原型:

    #include <sys/shm.h>
    
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    
    功能将物理内存区域映射到进程的虚拟地址空间(将物理地址转换为虚拟地址);
    参数shmid共享内存标识符;
    shmaddr将物理内存映射进程一个合理的位置(未使用,且内存足够大区域);
    shmflg一般为 0,对于共享内存段,可以实现读写;
    返回值成功返回链接物理内存的虚拟地址;
    失败返回 -1;
  3. 将物理内存与进程虚拟内存的映射关系断开,函数原型:

    #include <sys/shm.h>
    
    int shmdt(const void *shmaddr);
    
    功能将物理内存与进程的虚拟内存的映射关系断开;
    参数shmaddr映射的虚拟地址值;
    返回值成功返回 0;
    失败返回 -1;
  4. 释放物理内存,函数原型:

    #include <sys/shm.h>
    
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    功能实现对共享内存的控制;
    参数shmid共享内存标识符;
    cmd指定对共享内存的操作;
    IPC_STAT:从第三个参数中读取共享内存的属性信息;
    IPC_SET:设置共享内存的属性信息;
    IPC_RMID:删除共享内存;
    buf描述共享内存属性的结构;
    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(创建共享内存的用户ID) */
        pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2)(上一次执行映射和断开映射的进程ID) */
        shmatt_t        shm_nattch;  /* No. of current attaches(被映射的次数(编号)) */
        ...
    };
    返回值成功返回 0;
    失败返回 -1;

代码实例:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>

int main(int argc, const char argv[]){
	// 创建共享内存标识符
	int shmid = shmget(IPC_PRIVATE, 1000, IPC_CREAT|0600);
	if(shmid == -1){
		perror("shmget error!\n");
		exit(1);
	}
	printf("create shared memory OK. shmid = %d\n", shmid);

	// 物理内存与虚拟内存建立联系后写数据
	char * shmaddr = (char *)shmat(shmid, NULL, 0);
	if((int)shmaddr == -1){
		perror("shmat error!\n");
		exit(1);
	}
	strcpy(shmaddr, "hello world");
	shmdt(shmaddr);

	// 物理内存与虚拟内存建立联系后读数据
	shmaddr = (char *)shmat(shmid, NULL, 0);
	if((int)shmaddr == -1){
		perror("shmat error!\n");
		exit(1);
	}
	printf("%s\n", shmaddr);

	// 删除
	shmctl(shmid, IPC_RMID, NULL);
	return 0;
}

4.3 信号量集

Posix 信号量采用计数信号量,System V 信号量在此基础上增加了一级复杂度,它采用计数信号量集,计数信号量集是由一个或多个计数信号量构成的集合。

对于系统中的每个信号量集,内核都维护一个struct semid_ds信息结构,它定义在sys/sem.h头文件中。

struct semid_ds
{
    struct ipc_perm  sem_perm;
    struct sem       *sem_base;  //指向信号量集的指针
    ushort           sem_nsems;  //信号量集中的信号量个数
    time_t           sem_otime;  //上一次调用semop的时间
    time_t           sem_ctime;  //创建时间或上一次以IPC_SET调用semctl的时间
};

其中,sem_base 是指向信号量集的指针,信号量集中的每个成员都对应一个 struct sem 结构:

struct sem
{
    ushort_t  semval;  //信号量的值
    short     sempid;  //上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
    ushort_t  semncnt; //等待semval变为大于当前值的线程数
    ushort_t  semzcnt; //等待semval变为0的线程数
};
  1. 生成 key 值(键值),函数原型:

    #include <sys/ipc.h>
    
    key_t ftok(const char *pathname, int proj_id);
    
    功能生成键值;
    参数pathname路径名,用户给定,必须真实存在;
    proj_id传字符;可以根据自己的约定,随意设置。这个数字,有的称之为 project ID; 在UNIX 系统上,它的取值是 1 到 255;
    返回值成功返回 key 值(随机数);
    失败返回 -1;
  2. 信号量级的创建,函数原型:

    #include <sys/sem.h>
    
    int semget(key_t key, int nsems, int semflg);
    
    功能用于创建一个新的信号量集或打开一个已存在的信号量集;
    参数keyftok 返回值;
    nsems指定了信号量集中的元素个数(该参数只在创建信号量时有效);
    semflgsem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误;
    返回值成功返回信号量集的标识符;
    失败返回 -1,错误原因存于error中;
    错误代码EACCESS没有权限;
    EEXIST信号量集已经存在,无法创建;
    EIDRM信号量集已经删除;
    ENOENT信号量集不存在,同时 semflg 没有设置 IPC_CREAT 标志;
    ENOMEM没有足够的内存创建新的信号量集;
    ENOSPC超出限制;
  3. 改变信号量的值,函数原型:

    #include <sys/sem.h>
    
    int semop(int semid, struct sembuf *sops, size_t nsops);
    int semtimedop(int semid, struct sembuf *sops, size_t nsops, 
                   const struct timespec *timeout);
    
    功能完成对信号量的 P 操作或 V 操作;
    参数semid信号量集标识符;
    sops此结构体为:
    struct sembuf {
        unsigned short  sem_num;  /* 数组中的信号量下标 */
        short        sem_op;  /* 信号量操作 */
        short        sem_flg;  /* 操作标识 */
    };
    成员解读:
    sem_num:信号量集的个数,单个信号量设置为0;
    sem_op(PV操作):
    1)>0:增加信号量的值(发送操作);
    2)<0:减小信号量的值(等待操作);
    3)=0:表示对信号量当前值进行是否为 0 的测试;
    sem_flg:
    1)IPC_NOWAIT:如果不能对信号量集进行操作,则立即返回;
    2)SEM_UNDO:当进程退出后,该进程对 sem 进行的操作将撤销;
    nsops操作信号量的个数,即 sops 结构变量的个数;
    返回值成功返回 key 值(随机数);
    失败返回 -1;
  4. 对信号量集的控制,函数原型:

    #include <sys/sem.h>
    
    int semctl(int semid, int semnum, int cmd, ...);
    
    功能常用于设置信号量的初始值和销毁信号量;
    参数semid信号量标识符;
    sem_num信号量集数组上的下标,表示某一个信号量,填 0;
    cmd对信号量命令操作:
    1. IPC_RMID:销毁信号量,不需要第四个参数;
    2. SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下(相当于要对信号量设置属性的一个描述信息,将这个描述信息作为参数传给 semctl 函数);
    3. GETVAL:获取信号量值;
    // 用于信号灯操作的共同体。
      union semun
      {
        int val;
        struct semid_ds *buf;
        unsigned short *arry;
      };
    返回值成功情况略为复杂,见 API 手册;
    失败返回 -1;

代码实例:两个进程 PA.C(设置信号量的值,每隔 3s semval 的值减一)PB.C(每隔 1s 获取信号量的值)

  1. PA.c:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/sem.h>
    
    typedef union semun{
    	int val;
    }semun_t;
    
    int main(int argc, const char argv[]){
    	struct sembuf sb = {0, -1, IPC_NOWAIT};
    	// 生成key值
    	key_t key = ftok(".", 31);
    	if(key == -1){
    		perror("ftok error!\n");
    		exit(1);
    	}
    
    	// 使用键值获取信号量集ID
    	int semid = semget(key, 1, IPC_CREAT|0664);
    	if(semid == -1){
    		perror("semget error!\n");
    		exit(1);
    	}
    
    	// 使用信号量集ID设置信号量的初值
    	semun_t arg;
    	arg.val = 5;
    	int ctl = semctl(semid, 0, SETVAL, arg);
    	if(ctl == -1){
    		perror("semctl error!\n");
    		exit(1);
    	}
    
    	// 每隔3s第一个信号量的值-1
    	while(1){
    		sleep(3);
    		// 改变信号量的值
    		int op = semop(semid, &sb, 1);
    		if(op == -1){
    			perror("semop error, number < 0!\n");
    			return -1;
    		}
    	}
    	return 0;
    }
    
  2. PB.c:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    
    int main(int argc, const char *argv[]){
    	// 获取键值
    	key_t key = ftok(".", 31);
    	if(key == -1){
    		perror("ftok error!\n");
    		return -1;
    	}
    
    	// 使用键值获取信号量集的ID
    	int semid = semget(key, 1, IPC_CREAT|0664);
    	if(semid == -1){
    		perror("semget error!\n");
    		return -1;
    	}
    
    	while(1){
    		sleep(1);
    		int val = semctl(semid, 0, GETVAL);
    		if(val == -1){
    			perror("semctl error!\n");
    			return -1;
    		}
    		printf("semval = %d\n", val);
    	}
    	return 0;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值