IO进程学习笔记完整版

2、man手册

  1. 普通命令。
  2. 系统调用的函数。
  3. 库函数。
  4. 特殊文件。
  5. 文件格式。
  6. 游戏。
  7. 附加的一些变量

3、IO介绍

I:input 输入

O:output 输出

对文件的输入和输出 输入-》写文件,将文件中的内容写到内存中去

输出-》读文件,将内存中的内容读取到文件中

linux下一切皆文件

文件类型(bcd-lsp)

  1. b:块设备文件
  2. c:字符设备文件
  3. d:目录文件
  4. -:普通文件
  5. l:链接文件
  6. s:套接字文件
  7. p:管道文件

4、标准IO和文件IO

4.1.文件IO

内核向上提供的输出输出函数接口,叫做系统调用函数接口。基于内核,内核不同,系统调用函数接口不同,文件IO不同操作系统函数接口不通用。可移植性较差。

4.2标准IO

标准IO是C库中定义的一组用于输入输出的函数接口。不同的操作系统只要移植了C库就可以使用,它是在系统调用之前做了一个二次封装,相当于是间接的进行了系统调用。可移植性强,可以在不同的系统环境下进行使用。

4.3标准IO的调用逻辑

标准IO读写
if(是linux操作系统)
{
    调用的就是linux的内核函数接口(文件IO)
}
if(是windows操作系统)
{
    调用的就是windows的内核函数接口(文件IO)
}
if(是macos操作系统)
{
    调用的就macos的内核函数接口(文件IO)
}

标准IO在系统调用之前作了二次封装增加了缓存机制,减少了系统调用的次数,提高了程序的效率。

正常的系统调用

应用层读写文件-》调用内核层的函数接口-》与硬件进行交互-》拿到数据返回给应用层-》每次读写重复

带有缓存机制

应用层读写文件-》调用内核层的函数接口-》与硬件进行交互-》拿到数据返回给应用层的缓存区-》每次读写从缓存区读取

5.标准IO

5.1.标准IO的特点

  1. 标准IO是c库中提供的一组专门用于输入输出的函数接口
  2. 标准IO由ANSI C标准定义,不仅能在Unix操作系统上,在很多的操作系统上都实现了标准IO
  3. 标准IO通过缓存机制·减少系统调用的次数,提高效率
  4. 标准IO围绕流进行操作(stream),在标准IO中,流用FILE *来描述。
  5. 标准IO默认打开三个流:标准输入(stdin),标准输出(stdout),标准出错(stderr)。

6.流

6.1定义

所有的I/O操作仅是简单的从程序移进或者移出,这种字节流,就称为流。

6.2分类:

文本流/二进制流。

6.3流指针FILE *

FILE* 是一个指向 FILE 结构体的指针,这个结构体由标准库定义,用于表示一个打开的文件或输入/输出流。

查看结构体:vi -t FILE

输入1

追:ctrl + ]

_IO_buf_end:缓存区的结束地址

_IO_buf_base:缓存区的起始地址

7.缓存区的分类

7.1全缓存-->基于文件:

刷新全缓存的方式:

1、   程序正常退出:

       遇到return (main)   退出

        exit:退出进程

       关闭流指针:fclose

2.fflush:强制刷新缓存

3.缓存区满

7.2行缓存 -->基于终端:stdin\stdout

刷新缓存区:

1.      程序正常退出   

        遇到return (main)

        exit:退出进程

       关闭流指针:fclose

2.fflush强制刷新缓存

3.缓存区满

4.\n刷新

7.3.不缓存: stderr

注意:缓存区只有在使用的时候才会开辟。

示例

非正常结束不刷新

\n刷新

缓存区满

fflush:强制刷新

#include <stdio.h>
int fflush(FILE *stream);
功能:刷新缓存区
参数:
    stream:流  (NULL:刷新所有流)
返回值:成功0
  失败EOF(-1),更新errno。
用法:fflush(NULL);

8.标准IO函数接口

1. 打开文件:fopen
2. 关闭文件:fclose
3.读/写单个字符:fgetc fputc
4. 读/写一串字符串:fgets fputs
5. 读/写一个二进制文件:fread fwrite
6. 移动指针:fseek

fopen打开文件)

  FILE *fopen(const char *path, const char *mode);

格式:  FILE *fopen(const char *path, const char *mode);

功能:打开文件 

参数:1、文件路径     2、打开方式

返回值:成功-的到文件流指针 失败:NULL,更新errno

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
功能:打开文件

fopen()函数打开由 pathname 指向的文件名对应的文件,并为该文件关联一个流。
参数“argument mode”指向一个以以下序列开头的字符串(可能还包含下面描述的其他字符):

参数:path:打开的文件的路径
      mode:打开方式
打开方式:
r:只读,文件指针定位到文件开头(有文件)
r+:可读可写,文件指针定位到文件开头(有文件)
w:只写,文件不存在创建,存在清空,文件指针定位到文件开头
w+:可读可写,文件不存在创建,存在清空,文件指针定位到文件开头
a: 只写,文件不存在创建,存在追加(到文件末尾)
a+:可读可写,文件不存在创建,存在追加(到文件末尾)
读文件指针定位到文件开头。
返回值:成功-的到文件流指针
        失败:NULL,更新errno

注意:打开文件后,若要对该文件进行操作,则只能打开一次。

示例代码:

问:一个任务中最多能打开多少个文件?(1024个)一个文件可不可以被重复的打开?(可)

注意:打开的文件属于有限资源,最多打开1024个,文件可以被重复打开,打开的文件不用了需要及时关闭

fclose关闭文件)

int fclose(FILE *stream);
功能:关闭文件
参数:
stream:流指针
返回值:成功0,失败-1,更新errno

示例代码
#include <stdio.h>
int main(int argc, char const *argv[])
{
    //打开一个名字叫test.c的文件,以只读的形式
    FILE *fp = fopen("./test.c","r");
    if (fp == NULL)
    {
        perror("fopen filed");
        return -1;
    }
    printf("open success\n");
    fclose(fp);
    return 0;
}

perror

#include <errno.h>
void perror(const char *s);
功能:根据errno值获取错误信息,将信息输出到终端
参数:S:提示内容
返回值:无

fgetc读取单个字符)

int fgetc(FILE *stream);
 功能:从文件中读一个字符
 参数:
    stream:流指针(从那个文件读)
返回值:成功返回读到字符的ascii值,失败返回或读到文件结尾返回-1.

示例代码:

fputc写单个字符)

int fputc(int c, FILE *stream);
功能:向指定的文件中写入一个字符
参数:
	c:要写入字符的ASCII值
	stream:流指针
返回值:写入字符ASCII值
	   失败返回:EOF

练习:用fgetc和fputc实现cp功能,将A文件中的内容放入B文件。

#include <stdio.h>
int main(int argc, char const *argv[])
{
    // 打开文件
    FILE *fp_src = fopen("./fopen.c", "r");
    if (fp_src == NULL)
    {
        perror("open src filed");
        return -1;
    }
    FILE *fp_dest = fopen("./test.c", "w");
    if (fp_dest == NULL)
    {
        perror("open dest filed");
        return -1;
    }
    //循环读取文件
    char ch;
    while ((ch = fgetc(fp_src)) != -1)
    {
        fputc(ch,fp_dest);
    }
    printf("读取完成\n");
    fclose(fp_src);
    fclose(fp_dest);
    
    return 0;
}

fprintf向指定文件写入数据)

#include<stdio.h>
 int fprintf(FILE *stream, const char *format, ...);
功能:向指定的文件以指定的格式写入数据
参数: stream :流指针
	    format:指定格式
		...:多个参数
返回值: 输出字符个数
	   失败返回:EOF

示例代码:
#include <stdio.h>
int main(int argc, char const *argv[])
{
    // 打开一个名字叫test.c的文件,以只读的形式
    FILE *fp = fopen("./a.txt", "w+");
    if (fp == NULL)
    {
        perror("fopen filed");
        return -1;
    }
    printf("open success\n");
    int age = 21;
    const char *name = "jj";
    double height = 156.666;
    fprintf(stdout,"age:%d\n",age);
    fprintf(fp,"name:%s\n",name);
    fprintf(fp,"height:%lf\n",height);

    fclose(fp);
    return 0;
}

fgets(获取字符串)

char *fgets(char *s, int size, FILE *stream);
功能:从文件中获取指定长度的字符串
参数: s:字符串存放的首地址
	 size:期望获取字符的个数   
	         实际读size-1个字符,会自动补'\0',预留一个位置补'\0'.
             文件中不满size-1个,有多少读多少,都会补'\0'.
             当读到'\n',结束,不再读下一行内容,再次调用fgets继续从下一行开始读。
           
	 stream:文件流指针
返回值:
	成功:返回获取成功字符串存放的首地址
	失败或读到文件结尾返回NULL。

注意:没有内容时会读阻塞

示例代码:
#include <stdio.h>
int main(int argc, char const *argv[])
{
    // 打开一个名字叫test.c的文件,以只读的形式
    FILE *fp = fopen("./a.txt", "r");
    if (fp == NULL)
    {
        perror("fopen filed");
        return -1;
    }
    printf("open success\n");
    char buf[64];
    while (fgets(buf, sizeof(buf), fp) != NULL)
    {
        printf("buf:%s", buf);
    }

    fclose(fp);
    return 0;
}

文件指针偏移函数

注意:由于我们进行操作时,文件的指针会位移,我们要注意这种情况,

1. rewind
void rewind(FILE *stream)
功能:将文件指针定位到文件开头
参数:流指针

2.ftell
long ftell(FILE *stream)
功能:计算文件指针当前的位置(相对于文件开头)
返回值:成功:返回当前文件指针相较于开头的字节数
        失败:-1;
    
3.fseek
int fseek(FILE *stream, long offset, int whence);
		功能:将文件指针偏移到指定位置
		参数:
		    stream:流指针
			offset:偏移量   +5 --》相对于位置向后偏移5byte
			                 -5 --》相对于位置向前偏移5byte
			whence:相对位置
			   SEEK_SET:开头
			   SEEK_CUR:当前
			   SEEK_END:结尾
		返回值:成功:0
		    失败:-1,更新errno
举例:
fseek(fp,-10,SEEK_END)
fseek(fp,-10,SEEK_CUR)

练习:使用ftell计算文件的长度

#include <stdio.h>
int main(int argc, char const *argv[])
{
    FILE *fp = fopen(argv[1],"r");
    if (fp == NULL)
    {
        perror("fopen filed");
        return -1;
    }
    //定位到文件的末尾
    fseek(fp,0,SEEK_END);
    //计算文件的长度
    long lenth = ftell(fp);
    printf("len:%ld",lenth);
    fclose(fp);
    return 0;
}

fputs(输入字符串)

int fputs(const char *s, FILE *stream);
   功能:向指定文件中输入一串字符
   参数:
         s:输入字符串的首地址
		 stream:文件流指针
	返回值:成功返回输出字符个数
	失败返回EOF

示例代码
#include <stdio.h>
int main(int argc, char const *argv[])
{
    // 打开文件
    FILE *fp = fopen(argv[1], "w");
    if (fp == NULL)
    {
        perror("fopen err");
        return -1;
    }
    char buf[64] = "我饿了,我想吃饭";
    //写入
    fputs(buf,fp);
    fclose(fp);
    return 0;
}

fread,fwrite(二进制形式读写)

 #include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以指定类型从文件中读取内容
参数: ptr:读内容存放的首地址
       size:读一个元素内容的字节大小
       nmemb:读元素的个数
       stream:流(文件)
返回值:成功:元素个数,读到文件结尾0.
     失败:-1

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
                     FILE *stream);
功能:以指定类型向文件中写内容
参数:
   ptr:写内容存放的首地址
    size:写一个元素内容的字节大小
    nmemb:写元素的个数
    stream:流(文件)
返回值:成功:写元素个数.
     失败:-1

练习:将一个int类型的数组写到文件中,并且将它读到另一个数组中

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int arr[5] = {1,2,3,4,5};
    int ary[5];
    //打开文件
    FILE *fp = fopen("./a.txt","w+");
    //写
    fwrite(arr,sizeof(int),5,fp);
    rewind(fp);
    //读
    fread(ary,sizeof(int),5,fp);
    for (int i = 0; i < 5; i++)
    {
        printf("ary:%d\n",ary[i]);
    }
    fclose(fp);
    return 0;
}

9.文件IO

9.1.概念

内核向上提供的输出输出函数接口,叫做系统调用函数接口。基于内核,内核不同,系统调用函数接口不同,文件IO不同操作系统函数接口不通用。可移植性较差。

9.2.文件IO的特点

  1. 没有缓存机制,每次调用都会引起系统调用。
  2. 围绕文件描述符进行操作,文件描述符都是非负的整数,依次进行分配。
  3. 文件IO默认打开三个文件描述符,0(标准输入),1(标准输出),2(标准错误)
  4. 可以操作除了目录文件外的任意类型的文件。

9.3.文件描述符

文件描述符都是非负的整数,取值范围(0-1023),最多产生1024个文件描述符,文件描述符被分配的时候是连续的。关闭文件描述符之后才可以提供给其他文件使用。

9.4.函数接口

9.4.1.open函数(打开文件)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
功能:打开文件
参数:pathname:文件路径名
      flags:打开文件的方式
            O_RDONLY:只读
            O_WRONLY:只写
            O_RDWR:可读可写
            O_CREAT:创建
            O_TRUNC:清 空
            O_APPEND:追加   
 返回值:成功:返回文件描述符
         失败:-1
         
fopen对应的open函数的打开方式
r:O_RDONLY
r+:O_RDWR
w:O_WRONLY | O_CREAT | O_TRUNC,0666
w+:O_RDWR | O_CREAT | O_TRUNC,0666
a:O_WRONLY | O_CREAT | O_APPEND,0666
a+: O_RDWR | O_CREAT | O_APPEND,0666

当文件的打开方式中存在   O_CREAT:创建 时
int open(const char *pathname, int flags, mode_t mode);
mode:创建文件的权限  ,只有在打开方式中有O_CREAT才填充这个参数
创建的文件实际的权限位mode & ~umask。

umask是权限掩码,值为0002。  如mode=0666,即创建的文件权限为0664
用于控制新文件或目录的默认权限的设置。它决定了在创建新文件或目录时,哪些权限会被屏蔽。

查看权限掩码:umask
修改权限掩码:umask 0000      
 
 

9.4.2.close函数关闭文件描述符)

 #include <unistd.h>
 int close(int fd);
 功能:关闭文件
 用法:close(fd);
 
 
 示例代码:
 #include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
    //打开文件
    int fd = open("./a.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1)
    {
       perror("open err");
       return -1;
    }
    printf("fd:%d\n",fd);
    //关闭文件
    close(fd);
    return 0;
}

9.4.3.read函数读文件)

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:读文件
参数:fd:文件描述符
      buf:存放内容的首地址
      count:期待读取字符的个数
返回值:成功实际读到字符的个数,读到文件结尾返回0
        失败返回-1,更新errno
        
注意事项:使用read函数时需要注意:read读文件,一般期待读多少字符就是读多少,不会自动补’\0‘,遇到\n也不会自动停止读取,会继续读下一行的内容。
'\0'需要自己补充,考虑预留一个字节补'\0'

使用技巧
1.通过返回值作为实际读到的字符个数,后面补‘\0’;
char buf[32];
int ret = read(fd,buf,31);
buf[ret] =  '\0';
2.每次读取的内容读取之前先清空数组
清空函数:bzero memset

9.4.4.write函数(写文件)

 ssize_t write(int fd, const void *buf, size_t count);
功能:写文件
参数:
    fd:文件描述符
    buf:写内容存放的首地址
    count:期待写字符的个数
返回值:成功实际写字符的个数
    失败返回-1,更新errno

 使用技巧:
int ret = read(fd,buf,32);
write(fd,buf,ret);

练习:实现cp,使用read和write

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    int fd_src = open("a.txt",O_RDONLY);
    if (fd_src == -1)
    {
        perror("open err");
        return -1;
    }
    int fd_dest = open("b.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd_dest == -1)
    {
        perror("open err");
        return -1;
    }
    //读取文件内容
    char buf[64];
    int ret = 0;
    while (ret = read(fd_src,buf,sizeof(buf)))
    {
        write(fd_dest,buf,ret);
    }
    return 0;
}

9.4.5.lseek(偏移函数)

#include <sys/types.h>
#include <unistd.h>

       off_t lseek(int fd, off_t offset, int whence);
	   功能:将文件指针移动到指定位置
	   参数:
	       fd:文件描述符
		   offset:偏移量  +向后 -向前
		   whence:相对位置
		       SEEK_SET:开头
			   SEEK_CUR:当前
			   SEEK_END:结尾
	返回值:成功:当前位置(基于文件开头)
	        失败:-1,更新errno
lseek(fd,0,SEEK_SET)

9.5.标准IO文件IO对比

标准IO

文件IO

概念

c库中定义的一组用于输入输出的函数接口

系统中定义的一组用于输入输出的函数接口

特点

  1. 标准IO是C库中提供的一组专门用于输入输出的函数接口
  2. 标准IO不仅在Unix系统上,在很多操作系统上都实现了标准IO。
  3. 标准I/O通过缓冲机制减少系统调用,提高效率
  4. 标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用FILE *来描述。
  5. 标准IO默认打开三个流:标准输入(stdin),标准输出(stdout),标准出错(stderr)。

  1. 没有缓存机制,每次调用都会引起系统调用。
  2. 围绕文件描述符进行操作,文件描述符都是非负的整数,依次进行分配。
  3. 文件IO默认打开三个文件描述符,0(标准输入),1(标准输出),2(标准错误)
  4. 可以操作除了目录文件外的任意类型的文件。

函数:

打开文件:fopen

关闭文件:fclose

读写操作:fgetc fputc fputs fgets fread fwrite

定位操作:fseek rewind ftell

时间:time localtime

其他:fflush fprintf perror

打开文件:open

关闭文件:close

读写操作:read write

定位操作:lseek

9.6.目录操作函数

9.6.1.opendir打开目录

#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开目录文件
参数:name:文件名
返回值:成功返回目录流指针
        失败返回NULL

示例代码;
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
    //打开当前目录
    DIR *dirp = opendir("./");
    if (dirp == NULL)
    {
        perror("opendir err");
        return -1;
    }
    return 0;
}

9.6.2.closedir关闭目录

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能:关闭目录
   参数:dirp:目录流指针
   返回值:成功0,失败-1,更新errno

9.6.3.readdir读取目录内容

#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:读取目录中的内容
参数:dirp:目录流指针
返回值:成功返回结构体指针,读到结尾返回NULL
    失败返回NULL,更新errno
    
struct dirent {
           ino_t      d_ino;       /* 文件的inode号 */
           off_t      d_off;       /* Not an offset; see below */
       unsigned short d_reclen;    /* Length of this record */
unsigned char  d_type;      /* 文件类型,但不是所有的都支持                                          
       char           d_name[256]; /*文件名 */
           };

9.6.4.chdir修改所处路径

chdir 修改当前所处路径
	 #include <unistd.h>
       int chdir(const char *path);
     功能:改变当前所处的路径
	 参数:path:修改的路径、
	 返回值:成功0
	      失败:-1,更新errno

示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    chdir("../");
    int fd = open("./aa.txt", O_RDWR | O_CREAT | O_TRUNC,0777);
    if (fd == -1)
    {
        perror("open err");
        return -1;
    }
    close(fd);
    return 0;
}

获取文件属性(stat)

man 2 stat

头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
函数接口
int stat(const char *pathname, struct stat *statbuf);
功能:获取文件属性
参数
    const char *pathname:文件名
    struct stat *statbuf:获取到的文件属性存放的位置
返回值:成功:0,失败-1,更新errno

struct stat {
   dev_t     st_dev;         /* ID of device containing file */
                                   包含的文件的设备ID
   ino_t     st_ino;         /* Inode number */文件的inode号
   mode_t    st_mode;        /* File type and mode */
                                       文件类型和权限
   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) */
                                   设备ID
   off_t     st_size;        /* Total size, in bytes */
                                   大小
   blksize_t st_blksize;     /* Block size for filesystem I/O */                                 文件系统IO块的大小
   blkcnt_t  st_blocks;      /* Number of 512B block allocated */                                 512B块设备的分配数量

   /* Since Linux 2.6, the kernel supports nanosecond
      precision for the following timestamp fields.
      For the details before Linux 2.6, see NOTES. */

       struct timespec st_atim;  /* Time of last access */
                                       最后访问的时间
       struct timespec st_mtim;  /* Time of last modification */                                     最后修改的时间
       struct timespec st_ctim;  /* Time of last status change */                                     最后状态改变的时间


打印inode号和设备ID
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{   
    struct stat sb;
    int fd_st = stat("./stat.c",&sb);
    if (fd_st == -1)
    {
        perror("stat err");
        return -1;
    }
    printf("dev:%ld\n",sb.st_dev);
    printf("inode:%ld\n",sb.st_ino);
    return 0;
}

 The following mask values are defined for the file type:
对于该文件的类型,定义了一下的掩码值
   S_IFMT     0170000   bit mask for the file type bit field
                          文件类型的位字段的位掩码
           S_IFSOCK   0140000   socket
           S_IFLNK    0120000   symbolic link
           S_IFREG    0100000   regular file
           S_IFBLK    0060000   block device
           S_IFDIR    0040000   directory
           S_IFCHR    0020000   character device
           S_IFIFO    0010000   FIFO
           
Thus, to test for a regular file (for example), one could write:
因此,要测试一个常规文件(例如),可以编写以下代码:
stat(pathname, &sb);
           if ((sb.st_mode & S_IFMT) == S_IFREG) {
               /* Handle regular file */
           }

示例代码:
if ((sb.st_mode & __S_IFMT) == __S_IFREG)
    {
        printf("普通文件\n");
    }
    
    
st_mode: 100664
S_IFMT    170000

001 000 000 110 110 100
&
001 111 000 000 000 000
 =
001 000 000 000 000 000
0100000

S_ISREG(m)  is it a regular file?

S_ISDIR(m)  directory?

S_ISCHR(m)  character device?

S_ISBLK(m)  block device?

S_ISFIFO(m) FIFO (named pipe)?

S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)

S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)

#define  S_ISSOCK(m)  : (m&s_IFMT) == S_IFREG   m相当于st_mode

用法:
if (S_ISREG(sb.st_mode))
    {
       printf("普通文件\n");
    }
    if (S_ISDIR(sb.st_mode))
    {
        putchar('d');
    }
    
权限:后9位控制
 S_IRUSR     00400   owner has read permission
 S_IWUSR     00200   owner has write permission
 S_IXUSR     00100   owner has execute permission

 S_IRGRP     00040   group has read permission
 S_IWGRP     00020   group has write permission
 S_IXGRP     00010   group has execute permission

 S_IROTH     00004   others have read permission
 S_IWOTH     00002   others have write permission
 S_IXOTH     00001   others have execute permission

用到谁就用st_mode & 谁就行
举例:
if(sb.st_mode & S_IRUSR)
{
    putchar('r’);
}
if(sb.st_mode & S_IWGRP)
{
    putchar('x‘);
}
使用方法:用谁与谁 st_mode & ???
     // 创作者权限
    if (st.st_mode & S_IRUSR)
        putchar('r');
    else
        putchar('-');

1.getpwuid:通过用户id获取用户名
     struct passwd *getpwuid(uid_t uid);
	
    struct passwd {
        char   *pw_name;       /* username */
        char   *pw_passwd;     /* user password */
        uid_t   pw_uid;        /* user ID */
        gid_t   pw_gid;        /* group ID */
        char   *pw_gecos;      /* user information */
        char   *pw_dir;        /* home directory */
        char   *pw_shell;      /* shell program */
    };
用法:
getpwuid(sb.st_uid)->pw_name

  2.getgrgid:通过组id获取组名
      struct group *getgrgid(gid_t gid);
  struct group {
      char   *gr_name;       /* group name */
      char   *gr_passwd;     /* group password */
      gid_t   gr_gid;        /* group ID */
      char  **gr_mem;        /* group members */
  };  
3. ctime():将时间转换为字符串的格式来表示
char *ctime(const time_t *timep);
转换为-"Wed Jun 30 21:49:08 1993\n"

用法:
ctime(&sb.st_mtim)
函数返回一个指向字符串的指针,这个字符串表示时间格式的日期和时间。标准的 ctime 函数返回的时间字符串格式如下:
Www Mmm dd hh:mm:ss yyyy\n\0

练习:实现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 sb;
    int fd_st = stat("./stat.c", &sb);
    if (fd_st == -1)
    {
        perror("stat err");
        return -1;
    }
    // 获取文件类型
    if (S_ISREG(sb.st_mode))
    {
        putchar('-');
    }
    if (S_ISDIR(sb.st_mode))
    {
        putchar('d');
    }
    // 获取文件权限
    // 创作者权限
    if (sb.st_mode & S_IRUSR)
    {
        putchar('r');
    }
    else
    {
        putchar('-');
    }
    // 同组用户权限
    if (sb.st_mode & S_IRGRP)
    {
        putchar('r');
    }
    else
    {
        putchar('-');
    }
    // 其他用户权限
    if (sb.st_mode & S_IROTH)
    {
        putchar('r');
    }
    else
    {
        putchar('-');
    }
    // 连接数
    printf("%ld ", sb.st_nlink);
    // 用户名
    printf("%s ", getpwuid(sb.st_uid)->pw_name);
    // 组名
    printf("%s ", getgrgid(sb.st_gid)->gr_name);
    // 访问时间
    printf("%.12s ",ctime(&sb.st_mtime) + 4);
    // 文件名
    printf("%s ","stat.c");
    printf("\n");
    return 0;
}

10.库

10.1.库的概念

就是把一些常用函数的目标文件打包在一起,提供相应函数的接口,便于程序员使用;本质上来说库是一种可执行代码的二进制形式

10.2.库的分类

静态库和动态库,本质区别是代码被载入时刻不同。

1) 静态库在程序编译时会被连接到目标代码中。

优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快

缺点:静态库中的代码复制到了程序中,因此体积较大;

静态库升级后,程序需要重新编译链接

2) 动态库是在程序运行时才被载入代码中。

优点:程序在执行时加载动态库,代码体积小;

程序升级更简单;

不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。

缺点:运行时还需要动态库的存在,移植性较差

动态库 静态库

linux: .so .a

windows: .dll .lib

10.3.库的制作

10.3.1.静态库的制作

1、将源文件编译生成目标文件

gcc -c xxx.c -o xxx.o

2、创建静态库,用ar 命令将.o文件生成.a文件

ar crs libxxx.a xxx.o

lib:库的前缀,xxx库名

3、链接库测试使用

gcc xxx.c -L指定库的路径 -l 指定的库名

10.3.2动态库的制作

  1. 创建与地址无关的目标文件

gcc -fPIC -c xxx.c -o xxx.o

  1. 创建动态库

gcc -shared -o libxxx.so xxx.o

  1. 测试动态库的使用

gcc xxx.c -L指定库的路径 -l 指定的库名

  1. 执行

解决办法

  1. 把库拷贝到/usr/lib

sudo cp libxxx.so /usr/lib

  1. 在LD_LIBRARY_PATH环境变量中加上库所在路径。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

注意:只在当前终端,终端关闭,环境变量消失。

  1. 添加/etc/ld.so.conf.d/*.conf文件

11.进程  PID  process

11.1进程程序区别

程序:1. 编译好的可执行的二进制文件。

2. 存放在磁盘空间是上的指令和数据的有序集合。

3. 程序是静态的,没有任何执行的概念

进程: 1. 一个独立的可调度的任务

2.执行一个程序所分配的资源总称

3. 进程是程序的一次完整执行过程

4. 进程是动态的,包括创建,调度,执行和消亡。

11.2进程的特

  • CPU调度进程时会给进程分配时间片(几毫秒~十几毫秒),当时间片用完后,cpu再进行其他进程的调度,实现进程的轮转,从而实现多任务的操作(什么是时间片)

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

11.3进程三段

linux当中的进程包含三个段

  1. 正文段:也叫代码段,用于存放被执行的机器指令。(通常是只读的)
  2. 用户数据段:存放进程在执行时直接进行操作的所有数据,包括进程使用的全部变量在内。
  • 系统数据段:有效地存放程序运行的环境,包括进程的控制信息等。

11.4进程的内存管理

进程是程序执行和管理资源的最小单位,包含的资源有:物理内存、文件描述符、虚拟地址 0G~4G,CPU时间片、进程号(PID唯一标识一个进程)

11.5进程的类型

  1. 交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。该类进程经常与用户进行交互,需要等待用户的输入,当接收到用户的输入后,该类进程会立刻响应,典型的交互式进程有:shell命令进程、文本编辑器等
  2. 批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。比如数据备份。
  3. 守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。

11.6进程的运行状态

查看进程状态:man ps

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

W paging (not valid since the 2.6.xx kernel)

X dead (should never be seen)

Z defunct ("zombie") process, terminated but not reaped by its

  • 运行态(R):此时正在运行或者准备运行的进程。
  • 睡眠态(等待态):可中断的睡眠态(S):处于等待状态中的进程,一旦被该进程等待的资源被释放,那么该进程就会进入运行状态。

不可中断的睡眠态(D):该状态的进程只能用wake_up()函数唤醒。

  • 暂停态(T):进程被暂停或者终止
  • 死亡态:进程结束 X
  • 僵尸态(Z):当进程已经终止运行,但还占用系统资源,要避免僵尸态的产生

11.7进程状态转换

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

11.8进程函数接口

11.8.1创建子进程fork)

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

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("creat err");
        return -1;
    }
    else if (pid == 0)
    {
        printf("in child\n");
    }
    else
    {
        printf("in parent\n");
    }
    return 0;
}


 

11.8.2.fork创建子进程的特点

  1. fork创建一个子进程,父进程返回子进程的pid,子进程中返回0。
  2. fork创建的子进程几乎拷贝了父进程所有的内容(三个段:堆栈、数据、代码),fork之前的代码被复制并不会被执行,fork之后的代码被复制并执行。
  3. fork创建进程一旦成功,进程之间的空间就相会独立。各自分配0-4G的虚拟内存空间。
  4. fork创建进程之前打开的文件可以通过复制拿到同一个文件描述符 操作同一个文件(同一个文件指针)。
  5. 如果父进程退出,子进程没有退出,子进程会变成孤儿进程被init进程收养。变成后台进程。如果子进程退出,父进程没有退出。父进程没有及时回收子进程的资源,子进程就会变成僵尸进程,应该避免僵尸进程的产生。defunct :僵尸进程的标志。

11.8.3fork和vfork(知道)

1.fork(): 子进程拷贝父进程的数据段,代码段

vfork(): 子进程与父进程共享数据段

2.fork(): 父子进程执行次序不确定

vfork(): 保证子进程先运行,在调用exec()或exit()之前,与父进程数据共享,在exec()或exit()调用之后,父进程才能运行

总结:fork: 更通用,适用于需要创建一个完全独立的子进程的场景,vfork: 更适用于子进程立即执行 exec() 覆盖其自身的场景,因为它避免了不必要的地址空间复制,提高了性能。

11.8.4.退出进程(exit,_exit

void exit(int status);
功能:结束进程,刷新缓存
参数:退出的状态
不返回。
exit(0);
void _exit(int status);
功能:结束进程,不会刷新缓存
参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。
    通常0表示正常结束;
    其他的数值表示出现了错误,进程非正常结束
    
两者的区别
exit();
刷新缓存,关闭所有打开的文件指针
_exit();
立即终止进程,但是不会进行上述的操作

exit(0)和return 0;
exit:函数,结束进程。
return:关键字,结束的是函数。

11.8.5.回收进程资源(wait,waitpid

pid_t wait(int *status); 
功能:回收子进程资源(阻塞)
参数:status:子进程退出状态,不接受子进程状态设为NULL
返回值:成功:回收的子进程的进程号
        失败:-1
        
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

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

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("creat err");
        return -1;
    }
    else if (pid == 0)
    {
        printf("in child\n");
        sleep(5);
        printf("子进程结束\n");
    }
    else
    {
        printf("in parent\n");
        waitpid(-1,NULL,WNOHANG);
        printf("%d\n",cpid);
        printf("子进程资源已回收\n");
    }
    return 0;
}


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

wait(NULL) = waitpid(-1,NULL,0) 阻塞回收任意子进程
waitpid(-1,NULL,WNOHANG):不阻塞回收任一子进程
waitpid(pid,NULL,0)阻塞回收指定的子进程

11.8.6获取进程号(getpid,getppid)

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

11.9.守护进程

11.9.1.特点

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

11.9.2.创建按步骤

  1. 创建子进程,父进程退出

让子进程变成孤儿进程,成为后台进程;fork()

  1. 在子进程中创建新会话

让子进程成为会话组组长,为了让子进程完全脱离终端;setsid()

  1. 改变进程运行路径为根目录

因为进程运行的路径不能被删除或卸载;chdir("/")

  1. 重设文件权限掩码

增大进程创建文件时权限,提高灵活性;umask(0)

  1. 关闭文件描述符

将不需要的文件关闭;close()

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

int main(int argc, char const *argv[])
{
    // 1.创建子进程
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    if (pid == 0)
    {
        //2.在子进程中创建会话
        setsid();
        //3. 改变运行路径
        chdir("/");
        //4. 重设文件权限掩码
        umask(0);
        //5.关闭文件描述符
        for (int i = 0; i < 3; i++)
        {
            close(i);
        }
        while (1);
    }
    else
    {
        exit(0);
    }
    return 0;
}

11.9.3.练习

创建一个守护进程,循环间隔1s向文件中写入一串hello。

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

int main(int argc, char const *argv[])
{
    // 1.创建子进程
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    if (pid == 0)
    {
        // 2.在子进程中创建会话
        setsid();
        // 3. 改变运行路径
        chdir("/");
        // 4. 重设文件权限掩码
        umask(0);
        // 5.关闭文件描述符
        for (int i = 0; i < 3; i++)
        {
            close(i);
        }
        // 打开一个文件
        int fd = open("./a.txt", O_RDWR | O_CREAT | O_APPEND, 0777);
        if (fd < 0)
        {
            perror("open err");
            return -1;
        }
        while (1)
        {
            write(fd,"hello\n",6);
            sleep(1);
        }
    }
    else
    {
        exit(0);
    }
    return 0;
}

11.9.4.exec函数族

作用:在当前进程中执行一个新的进程

1. int execl(const char *path, const char *arg, ... /* (char *) NULL */); 使用可执行文件的绝对路径或相对路径 path 来替换当前进程的映像。参数通过变长参数列表传递,必须以 NULL 结尾。

2. int execlp(const char *file, const char *arg, ... /* (char *) NULL */); 类似 execl,但 file 只需要是可执行文件的名称,函数会根据环境变量 PATH 查找可执行文件的路径。

3. int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); 与 execl 类似,但允许传递一个自定义的环境变量列表 envp。这个环境会代替当前进程的环境变量。

4. int execv(const char *path, char *const argv[]); 使用路径 path 和参数数组 argv[] 替换当前进程。argv[0] 是程序名,argv[] 数组必须以 NULL 结束。 5. int execvp(const char *file, char *const argv[]); 类似 execv,但会根据 PATH 环境变量查找可执行文件。 6. int execvpe(const char *file, char *const argv[],char *const envp[]); 与 execvp 类似,但允许使用自定义的环境变量 envp。

12.线程 TID  Thread

12.1.概念

线程是一个轻量级的进程,为了提高系统的性能引入线程,线程和进程都参与统一的调度。

12.2进程和线程的区别

共性:

都为操作系统提供了并发执行能力。

不同点:

  1. 调度和资源上:线程是系统调度的最小单位,进程是资源分配的最小单位。
  2. 地址空间上:同一个进程创建多个线程共享进程资源,进程的地址空间相互独立。
  3. 通信方面:线程的通信相对简单,只需要通过全局变量就能实现。但是需要考虑临界资源(临界资源包括同一个文件,全局变量等)访问的问题,进程间的通信相对复杂,需要借助进程间的通信机制(借助3g-4g的的内核空间)
  4. 安全性方面:线程的安全性相对较差,当进程结束时会导致所有线程退出,进程相对于安全。

12.3线程资源

共享的资源:可执行的指令,静态的数据,进程中打开的文件描述符,信号处理函数。当前的工作目录,用户的ID,用户的组ID

私有资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈、错误号 (errno)、信号 掩码和优先级、执行状态和属性

线程标识:

主线程的 TID 和 PID 是相同的,每个子线程有自己独立的 TID,但它们都共享相同的 PID

12.4.线程的函数接口

12.4.1.创建线程(pthread_create)

头文件

#include <pthread.h>

int pthread_create(pthread_t *thread , const pthread_attr_t *attr,void *(*start_routine) (void *) , void *arg);

功能:创建一个线程

参数:

1. pthread_t *thread:线程标识,成功创建线程后,pthread_create 会将新线程的 ID 写入 thread 指向的内存位置。

2. const pthread_attr_t *attr:线程属性, NULL:代表设置默认属性

3.  void *(*start_routine):函数名,代表线程函数,指向一个函数的指针,这个函数就是线程的执行体(也就是线程的入口函数)。该函数必须符合 void *(*start_routine)(void *) 的原型,即接受一个 void * 类型的参数,并返回一个 void * 类型的值。

4.void *arg:传递给 start_routine 的参数。arg 是一个通用的指针,可以传递任何类型的数据(通常是一个结构体的指针,以便传递多个参数)。如果不需要传递参数,可以传递 NULL。

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

#include <stdio.h>

#include <pthread.h>

 

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

{

pthread_t tid; //创建一个线程

if(pthread_create(&tid,NULL,mythread,NULL))

{

printf("create failed\n"); return -1;

}

printf("主线程执行中。。。\n");

sleep(2); printf("主线程执行结束\n");

return 0; }

线程函数和普通函数的区别

1.普通函数是顺序执行的,手动调用。线程函数在创建线程的时候并发执行,不会影响主程序的运行

2.普通函数同步执行,线程函数异步执行

3. 存储位置不同

12.4.2.线程退出函数(pthread_exit)

void pthread_exit(void *retval);
功能:用于执行退出线程
参数:任意类型的数据,一般写NULL
返回值:无
pthread_exit(NULL);

通过 retval 参数,当前线程可以向其他线程返回执行状态或结果。这个状态信息可以通过 pthread_join 函数获取。

12.4.3.线程回收函数(pthread_join/pthread_detach)

int pthread_join(pthread_t thread, void **retval);
功能:用于等待一个指定的线程结束,阻塞函数
参数:1、thread:创建的线程对象 
      2、void **retval: 指针*retval指向线程返回的参数,一般是NULL
返回值:成功 : 0
       失败:errno
       

int pthread_detach(pthread_t thread);
功能:让线程结束时自动回收线程资源,让线程和主线程分离
参数:1、thread:线程ID
返回值:成功 : 0
       失败:errno

#include <stdio.h>
#include <pthread.h>
//线程函数
void *thread(void *arg)
{
    int *num = (int *)arg;
    printf("子线程执行中\n");
    sleep(4);
    pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
    //创建一个线程
    pthread_t tid;  //线程标识
    int value = 10;
    if(pthread_create(&tid,NULL,thread,(void*)&value))
    {
        printf("creat err");
        return -1;
    }
    //不阻塞等待,分离子线程,自己回收
    pthread_detach(tid);
    阻塞回收线程
    pthread_join(tid,NULL);
    printf("线程接着执行\n");
    return 0;
}

12.4.4.获取线程号(pthread_self)

pthread_t pthread_self(void);
功能:
    获取线程号
返回值:
    成功:调用此函数线程的ID

12.5.线程同步

线程之间是很容易进行通信的,能够通过全局变量实现数据的共享和交换,也就是通过访问临界资源,但是多个线程在同时访问共享数据的对象时需要引入同步和互斥机制。

临界资源:一次仅允许一个线程访问的资源

12.5.1.概念

同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情

12.5.2.线程的通信机制

12.5.2.1.信号量

在linux当中信号量的分类

  1. 内核信号量

由内核控制路径使用

  1. Posix信号量

a.无名信号量:数据存储在内存中,通常在线程间使用或父子进程间

函数接口:sem_init\sem_wait\sem_post

b.有名信号量:数据存储在文件中,在进程间线程间都可以使用

函数接口:sem_open\sem_wait\sem_post\sem_close

  1. System V信号量

是信号量的集合,叫信号灯集,属于IPC对象

函数接口:semget\semctl\semop

12.5.2.2无名信号量

信号量:通过信号量实现同步操作,由信号量决定线程是继续运行还是阻塞等待。

信号量代表的是某一类资源,它的值表示系统中该资源的数量,信号量>0的话,表示有资源可以使用,可以申请到资源,继续执行程序,信号量<= 0的话,表示没有资源可以使用,无法申请到资源,阻塞。

信号量是一个受保护的量,只能通过三种操作来访问:

  1. 初始化信号量:sem_init()
  2. P操作,申请资源:sem_wait() 资源-1
  3. V操作,释放资源:sem_post() 资源+1

注意:信号量是一个非负的整数,所以一定是(>=0)

12.5.2.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,函数不阻塞


销毁信号量
int sem_destroy(sem_t *sem)
功能:销毁信号量
参数:信号量对象
返回值:成功:0
        失败:-1 错误码

示例代码

#include <stdio.h>
#include <semaphore.h>
int main(int argc, char const *argv[])
{
    sem_t sem;
    int sval = 0;
    // 初始化信号量
    if (sem_init(&sem, 0, 2)) // if(0)
    {
        perror("init filed\n");
        return -1;
    }
    // 获取信号量的值
    sem_getvalue(&sem, &sval);
    printf("init semvalue :%d\n", sval);
    // 申请资源
    sem_wait(&sem);  //-1
    sem_wait(&sem);  //-1
    sem_wait(&sem);
    sem_wait(&sem);
    sem_wait(&sem);
    sem_getvalue(&sem, &sval);
    printf("init semvalue :%d\n", sval);
    sem_post(&sem);
    sem_getvalue(&sem, &sval);
    printf("init semvalue :%d\n", sval);
    return 0;
}

获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
功能:获取信号量的值
参数:sem_t *sem:信号量对象
      int *sval:信号量的值存放的变量
返回值:成功:0
        失败:错误码:

终端输入,终端输出

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
char buf[32];
sem_t sem;
void *myprint(void *arg)
{
    while (1)
    {
        sem_wait(&sem);                          //0
        if (!strcmp(buf, "quit"))
        {
            pthread_exit(NULL);
        }
        printf("buf:%s\n", buf);
    }
}
int main(int argc, char const *argv[])
{
    // 创建一个线程
    pthread_t tid;
    // 初始化信号量
    if (sem_init(&sem, 0, 0))           //0
    {
        perror("init filed\n");
        return -1;
    }
    if (pthread_create(&tid, NULL, myprint, NULL))
    {
        printf("create err\n");
        return -1;
    }
    while (1)
    {
        // 从终端获取内容
        fgets(buf, 32, stdin);
        if (buf[strlen(buf) - 1] == '\n')
        {
            buf[strlen(buf) - 1] = '\0';
        }
        sem_post(&sem);                             //1
        if (!strcmp(buf, "quit"))
        {
            break;
        }
    }
    pthread_join(tid, NULL);
    return 0;
}

12.6.互斥概念

mutex  互斥  pthread :线程

互斥:多个线程在访问临界资源时,同一时间只能一个线程进行访问

12.6.1.互斥锁

12.6.1.1.概念

互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。

12.6.1.2.函数接口

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

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

练习:

两个线程实现一个循环倒置,一个循环打印

int arr[10] = {0~9}

#include <stdio.h>
#include <pthread.h>
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
// 打印线程
void *myprint(void *arg)
{
    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);
    }
    
}
// 倒置线程
void *myrever(void *arg)
{
    int temp;
    while (1)
    {
        //上锁
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 5; i++)
        {
            temp = a[i];
            a[i] = a[9 - i];
            a[9 - i] = temp;
        }
        //解锁
        pthread_mutex_unlock(&lock);
    }
}
int main(int argc, char const *argv[])
{
    //初始化互斥锁
    if(pthread_mutex_init(&lock,NULL))
    {
        printf("init err\n");
        return -1;
    }
    // 创建两个线程
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, myprint, NULL))
    {
        printf("err1\n");
        return -1;
    }
    if (pthread_create(&tid2, NULL, myrever, NULL))
    {
        printf("err2\n");
        return -1;
    }
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);

    return 0;
} 
12.6.1.3.死锁(面试可能会问)

概念:

是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

产生死锁的原因:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

12.7.条件变量

一般和互斥锁搭配使用,用来实现线程的同步

12.7.1.使用步骤

  1. pthread_cond_init:初始化条件变量
  2. pthread_cond_wait:阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当有条件产生时,结束阻塞,同时上锁。
  3. pthread_cond_signal:产生条件,不阻塞

pthread_cond_wait必须先执行,pthread_cond_signal才能执行,产生条件

  1. 销毁条件变量

12.7.2.函数接口

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化条件变量
参数:cond:是一个指向结构pthread_cond_t的指针
    restrict attr:是一个指向结构pthread_condattr_t的指针,一般设为NULL
返回值:成功:0 失败:非0
 
 
int pthread_cond_wait(pthread_cond_t *restrict cond,    pthread_mutex_t *restrict mutex);
功能:等待信号的产生
参数:restrict cond:要等待的条件
     restrict mutex:对应的锁
返回值:成功:0,失败:不为0
注:当没有条件产生时函数会阻塞,同时会将锁解开;如果等待到条件产生,函数会结束阻塞同时进行上锁。


int pthread_cond_signal(pthread_cond_t *cond);
功能:给条件变量发送信号
参数:cond:条件变量值
返回值:成功:0,失败:非0
注:必须等待pthread_cond_wait函数先执行,再产生条件才可以


int pthread_cond_destroy(pthread_cond_t *cond);
功能:将条件变量销毁
参数:cond:条件变量值
返回值:成功:0, 失败:非0

练习

利用条件变量和互斥锁实现同步操作

#include <stdio.h>
#include <pthread.h>
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
pthread_cond_t cond;
// 打印线程
void *myprint(void *arg)
{
    while (1)
    {

        pthread_mutex_lock(&lock);   //锁住资源
        pthread_cond_wait(&cond,&lock); //没有条件产生阻塞,解锁
        for (int i = 0; i < 10; i++)
        {
            printf("%d ",a[i]);
        }
        printf("\n");
        pthread_mutex_unlock(&lock);
    }
    
}
// 倒置线程
void *myrever(void *arg)
{
    int temp;
    while (1)
    {
        sleep(1);
        //上锁
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 5; i++)
        {
            temp = a[i];
            a[i] = a[9 - i];
            a[9 - i] = temp;
        }
        //产生条件
        pthread_cond_signal(&cond);
        //解锁
        pthread_mutex_unlock(&lock);


    }
}
int main(int argc, char const *argv[])
{
    //初始化互斥锁
    if(pthread_mutex_init(&lock,NULL))
    {
        printf("init_mutex err\n");
        return -1;
    }
    //初始化条件变量
    if (pthread_cond_init(&cond,NULL))
    {
        printf("init_cond err\n");
        return -1;
    }
    // 创建两个线程
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, myprint, NULL))
    {
        printf("err1\n");
        return -1;
    }
    if (pthread_create(&tid2, NULL, myrever, NULL))
    {
        printf("err2\n");
        return -1;
    }
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);
     pthread_cond_destroy(&cond);
    return 0;
}

Linux/Unix平台的通信方式发展

1.早期通信方式

2.AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。IPC:InterProcess Communication

  1. BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,形成了基于套接字(socket)的进程间通信机制。

13.进程间的通信方式

  1. 早期的进程间的通信

无名管道(pipe) 有名管道(fifo) 信号(sem)

  1. system V IPC对象

共享内存(share memory) 消息队列(message queue) 信号灯集(semaphore)

  1. BSD

套接字(socket)

13.1.无名管道

13.1.1.无名管道原理图

13.1.2.无名管道特点

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

(单工:只能单方向传输信息 (广播、电视)

半双工:在同一时刻,只能一个方向传输信息(对讲机)

全双工:可以双向同时传输信息(电话))

注意:创建无名管道必须在fork之前     并且管道不支持偏移

13.1.3.函数接口

int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
      失败 -1
      
      
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    int fd[2];
    //创建管道
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    //创建子进程
    
    return 0;
}

13.1.4.注意事项(读写特性)

  1. 当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
  2. 管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续,直到写满为止

13.2.有名管道

13.2.1.特点

  1. 有名管道可以使互不相关的两个进程互相通信。
  2. 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。
  3. 进程通过文件IO来操作有名管道
  4. 有名管道遵循先进先出规则,不支持如lseek() 操作

13.2.2.函数接口

int mkfifo(const char *filename,mode_t mode);

`mkfifo()` 会在路径名指定的位置创建一个命名管道特殊文件。 


功能:创建有名管道
参数:1、filename:有名管道文件名
       2、mode:权限
通常会受到进程 umask 的影响:创建的文件的权限为 (`mode` & ~umask)。

返回值:成功:0
       失败:-1,并设置errno号
       
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    // 创建一个有名管道
    if (mkfifo("./fifo", 0666) < 0)
    {
        if (errno != EEXIST )
        {
            perror("fifo err");
            return -1;
        }
    }
    return 0;
}

13.2.3.注意事项(读写特性)

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

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

无名管道

有名管道

使用场景

具有亲缘关系的进程间

不相关两个进程可以使用

特点

看做一种特殊文件,通过文件IO操作

固定读端fd[0]和写端fd[1]

不支持lseek操作,遵循先进先出

在文件系统中会存在管道文件,数据存放在内核空间

通过文件IO进行操作

不支持lseek操作,遵循先进先出

函数

pipe()

直接read、write

mkfifo()

先打开open,再读写read/write

读写特性

当管道中没有数据时,读阻塞,写端关闭,读立即返回

当管道写满数据时,写阻塞

读端关闭,往管道写数据导致管道破裂

可读可写,如果管道中没有数据,读阻塞

只写方式,写阻塞,一直到另一个进程把读打开

只读方式,读阻塞,一直到另一个进程把写打开

练习

使用互不相干两个进程来完成cp功能

进程1

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


int main(int argc, char const *argv[])
{
    char buf[32];
    ssize_t ret;
    // 创建有名管道
    if (mkfifo("./fifo",0666) < 0)
    {
        if (errno != 17)
        {
            perror("fifo err");
            return -1;
        }
    }
    printf("fifo ok\n");
    // 打开目标文件和管道文件
    int fd_src = open(argv[1], O_RDONLY);
    if (fd_src < 0)
    {
        perror("open fd_src err");
        return -1;
    }
    printf("原文件打开成功\n");
    int fd_fifo = open("./fifo", O_WRONLY);
    if (fd_fifo < 0)
    {
        perror("open fd_fifo err");
        return -1;
    }
    // 循环读写
    while (ret = read(fd_src,buf,32))
    {
        write(fd_fifo,buf,ret);
    }
    printf("复制成功\n");
    // 关闭文件描述符
    close(fd_src);
    close(fd_fifo);
    return 0;
}

进程2

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

int main(int argc, char const *argv[])
{
    char buf[32];
    ssize_t ret;
    //创建有名管道
    if (mkfifo("./fifo",0666) < 0)
    {
        if (errno != 17)
        {
            perror("fifo err");
            return -1;
        }
    }
    printf("fifo ok\n");
    //打开目标文件和管道文件
    int fd_dest = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd_dest < 0)
    {
        perror("fd_dest err");
        return -1;
    }
    printf("目标文件打开成功\n");
    int fd_fifo = open("./fifo", O_RDONLY);
    if (fd_fifo < 0)
    {
        perror("open fd_fifo err");
        return -1;
    }
    //循环读写
    while (ret = read(fd_fifo,buf,32))
    {
        write(fd_dest,buf,ret);
    }
    printf("复制成功\n");
    //关闭文件描述符
    close(fd_dest);
    close(fd_fifo);
    return 0;
}

13.3.信号

13.3.1.信号的概念

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

13.3.2.信号的响应方式

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

SIGINT:结束进程,对应快捷方式ctrl+c
SIGQUIT:退出信号,对应快捷方式ctrl+\
SIGKILL:结束进程,不能被忽略不能被捕捉
SIGALRM:闹钟信号,alarm函数设置定时,当到设定的时间时,内核会向进程发送此信号结束进程。
SIGTERM:结束终端进程,kill 使用时不加数字默认是此信号
SIGCHLD:子进程状态改变时给父进程发的信号
SIGSTOP:暂停进程,不能被忽略不能被捕捉
SIGTSTP:暂停信号,对应快捷方式ctrl+z

13.3.3.信号的种类

在Linux中,信号被分为不可靠信号和可靠信号,一共64种,可以通过kill -l命令来查看

  • 不可靠信号:也称为非实时信号,不支持排队,信号可能会丢失,比如发送多次相同的信号,进程只能收到一次,信号值取值区间为1~31
  • 可靠信号:也称为实时信号,支持排队,信号不会丢失,发多少次,就可以收到多少次,信号值取值区间为32~64

信号产生的方式有如下几种:

  • 对于前台进程,用户可以输入特殊终端字符来发送,比如输入Ctrl+C
  • 系统异常,比如浮点异常和非法内存段访问(段错误)
  • 系统状态变化,比如alarm定时器到期时将引起SIGALRM信号
  • 在终端运行kill命令或在程序中调用kill函数 kill -9 PID:杀死进程

nt kill(pid_t pid, int sig);
功能:信号发送
参数:1、pid:指定进程
      2、sig:要发送的信号
返回值:成功 0     
       失败 -1
       
int raise(int sig);
功能:进程向自己发送信号
参数:1、sig:信号
返回值:成功 0   
      失败 -1
      
int pause(void);         pause   暂停
功能:用于将调用进程挂起,直到收到信号为止。

unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则
返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时
已设置过闹钟时间,则之前的闹钟时间被新值所代替


#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    printf("hello\n");
    printf("%d\n",alarm(3));
    printf("nihao\n");
    sleep(2);
    alarm(5);
    while (1);
    return 0;
}

13.3.4.信号处理函数

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

13.4.进程间通信方式(System V)

13.4.1.共享内存

13.4.1.1.共享内存的特点

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

13.4.1.2.共享内存的实现原理

13.4.1.2.1.虚拟地址与物理地址
  1. 虚拟地址:每个进程在运行时认为自己有一块完整、连续的内存空间。这些内存地址被称为虚拟地址。进程直接访问和操作的是这些虚拟地址,认为它们是独立的。
  2. 物理地址:物理内存是真实存在的硬件资源,所有进程的虚拟地址都需要通过映射机制与实际的物理地址相对应。
  3. 共享内存是操作系统提供的一块可以被多个进程同时访问的物理内存区域。每个进程都可以通过映射相同的物理内存地址到自己的虚拟地址空间,从而实现对共享内存的访问。

了解内容:

虚拟地址到物理地址的映射

虚拟地址到物理地址的映射是由操作系统的内存管理单元(MMU)来完成的。这个映射过程通过页表(Page Table)进行,页表负责记录虚拟地址和物理地址的对应关系。

  • 页表:系统为每个进程维护一个页表,用来记录该进程的虚拟内存和物理内存的映射关系。每当进程访问某个虚拟地址,MMU会通过页表找到对应的物理地址。
  • 页帧:虚拟内存被划分为固定大小的页(Page),物理内存也被划分为相同大小的页帧。虚拟页通过页表被映射到物理页帧。
  • 页面置换:当物理内存不足时,操作系统可以将不常用的物理内存页交换到硬盘上的交换分区,腾出空间给其他进程使用。

操作过程

  1. 进程发出内存访问请求:当一个进程要读取或写入内存中的某个数据时,它会提供虚拟地址。
  2. MMU 进行地址转换:内存管理单元(MMU)通过查询页表,将虚拟地址转换为对应的物理地址。
  3. 物理内存操作:一旦找到正确的物理地址,操作系统就可以在物理内存中读取或写入数据。

缺页中断(Page Fault):如果该虚拟地址对应的物理地址不在内存中(例如已经被交换到磁盘),则会触发缺页中断,操作系统会将对应的页从磁盘中加载回内存

13.4.1.3.共享内存的创建步骤

  1. 创建独一无二的key值 ftok 
  2. 创建或打开共享内存 shmget                 shared memory    get
  3. 映射共享内存到用户空间 shmat             shared  memory  at
  4. 撤销映射:shmdt                                    shared  memory dt
  5. 删除共享内存 shmctl                               shared memory  ctl
13.4.1.4.函数接口

创建独一无二的key值 ftok

key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
   1、 Pathname:已经存在的可访问文件的名字
   2、Proj_id:一个字符(因为只用低八位)
返回值:成功:key值
      失败:-1 

 ls  -i:查看文件的inode号

key值是根据pathname(文件路径)的inode号(只显示四位十六进制)和proj_id(一个字符)的低8位组合而成的。如:0x61011944,pathname只要是路径中存在的文件即可。

例:

创建或打开共享内存 shmget

int shmget(key_t key, size_t size, int shmflg);

功能:创建或打开共享内存

参数

1、 key 键值

2、 size 共享内存的大小

2、shmflg    IPC_CREAT|IPC_EXCL(判错)|0666 IPC_CREAT:

如果共享内存段不存在,创建一个新的共享内存段。 IPC_EXCL:如果共享内存段已经存在,那么返回错误。如果不加 IPC_EXCL,而共享内存段存在,shmget 会直接返回该段的标识符。

返回值:成功 shmid 出错 -1

例:

查看共享内存命令:ipcs -m

删除共享内存:ipcrm -m shmid

运行两次编译好的文件会出现报错的情况,共享内存已经存在,使用上述两条命令可以查看并且删除共享内存。或者进行代码优化

解决方案:

映射共享内存到用户空间 shmat

            void *shmat(int shmid,const void *shmaddr,int shmflg);

功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

参数:

           1、shmid 共享内存的id号

           2、 shmaddr 一般为NULL,表示由系统自动完成映射

             如果不为NULL,那么由用户指定

           3、shmflg:SHM_RDONLY就是对该共享内存只进行读操作

                   0 可读可写

返回值:成功:完成映射后的地址,

失败:-1的地址

取消映射

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

删除共享内存 shmctl

int  shmctl(int  shmid,int  cmd,struct  shmid_ds   *buf);
功能:(删除共享内存),对共享内存进行各种操作
参数:
    1、shmid   共享内存的id号
    2、cmd  IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可

    3、struct  shmid_ds   *buf:是一个结构体指针,但我们是删除共享内存,所以并没有意义,我们直接设置为NULL就可以了

返回:成功0 
     失败-1

13.4.1.5.共享内存完整代码

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

int main(int argc, char const *argv[])
{
    // 创建key
    key_t key = ftok("./key.c", 'b');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key = %#x\n", key);
    // 创建或打开共享内存
    int shmid = shmget(key, 256, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        if (errno == EEXIST)
        {
            shmid = shmget(key, 256, 0666);
        }
        else
        {
            perror("shmid create err");
            return -1;
        }
    }
    printf("shmid = %d\n", shmid);
    //建立映射关系
    char *p = shmat(shmid,NULL,0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }
    //读写共享内存
    //strcpy(p,"hello world");
    scanf("%s",p);
    printf("%s\n",p);
    //取消映射
    shmdt(p);
    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

13.4.信号灯集

13.4.1.概念

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

通过信号灯集实现共享内存的同步操作

13.4.2.创建步骤

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

查看信号灯集:ipcs -s

删除信号灯集:ipcrm -s semid

13.4.3.函数接口

13.4.3.1.创建或者打开信号灯集: semget

int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT |IPC_EXCL |0666

返回值:成功:信号灯集ID
       失败:-1
       
       
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <sys/sem.h>

int main(int argc, char const *argv[])
{
    // 创建key
    key_t key = ftok("./key.c", 'u');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key = %#x\n", key);
    // 创建或打开信号灯集
    int 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;
        }
    }
    printf("semid = %d\n", semid);
    return 0;
}

查看信号灯集:ipcs -s
删除信号灯集:ipcrm -s semid
13.4.3.2.初始化信号灯集 semctl
int semctl ( int semid, int semnum,int cmd,…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号
     cmd:
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
    ...:当cmd为SETVAL时,需要传递共用体
返回值:成功 0
      失败 -1
共用体格式:  
    union semun {
       int              val;    /* 信号量的初值 */
       struct semid_ds *buf;    /*存放信号量的一些相关信息 */
       unsigned short  *array;  /* 获取或设置信号量集中所有信号量的值*/
       struct seminfo  *__buf;  /* 用于存储与 Linux 系统相关的信号量信息。
}
 */
 
13.4.3.3.PV操作 semop

int semop ( int semid, struct sembuf *opsptr, size_t nops);

功能:对信号灯集合中的信号量进行PV操作

参数:1、semid:信号灯集ID

2、opsptr:操作方式(用结构体操作 如下,该结构体已经封装 直接使用

struct sembuf

{

short sem_num; // 要操作的信号灯的编号

short sem_op; // 0 : 等待,直到信号灯的值变成0

// 1 : 释放资源,V操作

// -1 : 分配资源,P操作

short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO

//IPC_NOWAIT:如果操作无法立即执行,则不会阻塞进程,而是立即返回错误。

//SEM_UNDO:当进程终止时,会自动撤销对信号量的操作。这种方式确保了进程终止后//不留下资源锁。

}

3、nops: 要操作的信号灯的个数 1

返回值:成功 :0

           失败:-1

封装优化:

13.5.消息队列

13.5.1.特点

1)消息队列是IPC对象的一种,由消息队列ID来唯一标识

2)消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。

3)消息队列可以按照类型来发送(添加)/接收(读取)消息

4)消息队列是存在Linux内核中,以链表形式来存放消息,消息队列对每个数据都有一个最大长度的限制

13.5.2在linux操作系统当中,消息队列的限制如下

消息队列的个数最多为16个;

消息队列的容量最多为16384字节;

每个消息内容最多为8192字节;

13.5.3.创建步骤

  1. 创建一个独一无二的key(ftok)
  2. 创建或者打开消息队列(msgget)
  3. 添加消息:按照类型把消息添加到已打开的消息队列末尾msgsnd
  4. 读取消息:按照类型把消息从消息队列的开头读出来msgrcv
  5. 删除消息队列msgctl

13.5.4.函数接口

13.5.4.1.创建消息队列

#include <sys/msg.h>

int msgget(key_t key, int flag);

功能:创建或打开一个消息队列

参数: key值

flag:创建消息队列的权限:IPC_CREAT|IPC_EXCL|0666

            IPC_CREAT:如果共享内存段不存在,创建一个新的共享内存段。

            IPC_EXCL:如果共享内存段已经存在,那么返回错误。

返回值:成功:msgid

失败:-1

例:

13.5.4.2.发送消息msgsnd

int msgsnd(int msqid, const void *msgp, size_t size, int flag);

功能:添加消息

参数:

1、msqid:消息队列的ID

2、msgp:指向消息的指针。常用消息结构msgbuf如下:

struct msgbuf{

                              long mtype; //消息类型

                              char mtext[N]}; //消息正文

                             }

3、size:发送的消息正文的字节数

4、flag:IPC_NOWAIT消息没有发送完成函数也会立即返回

0:直到发送完成函数才返回

返回值:成功:0

失败:-1

例:

13.5.4.3.读取消息 msgrcv

ssize_t msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag);

功能:读取消息

参数:

1、msgid:消息队列的ID

2、msgp:存放读取消息的空间   写入时是什么类型,取的时候也应什么类型

3、size:接受的消息正文的字节数

4、msgtype:0:接收消息队列中第一个消息。

大于0:接收消息队列中第一个类型为msgtyp的消息.

小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。

6、flag:0:若无消息函数会一直阻塞

IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG

返回值:成功:接收到的消息的长度

失败:-1

例:

注意:当接受

13.5.4.4.删除消息队列

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );

功能:对消息队列的操作,删除消息队列

参数:

1、msqid:消息队列的队列ID

2、cmd:

IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。

IPC_SET:设置消息队列的属性。这个值取自buf参数。

IPC_RMID:从系统中删除消息队列。

3、buf:消息队列缓冲区

返回值:成功:0

失败:-1

完整代码:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值