[ubuntu安装vmtools教程以及显示unable to execute](ubuntu安装wmwaretools教程以及显示 unable to execute “usr/bin/vmware-uninstall-tools.pl”解决办法 - 傻逼离我远点 - 博客园 (cnblogs.com))

使用 <font> 的标签的修改文字前景色

红色
绿色
蓝色

使用 rgb 颜色值

使用十六进制颜色值

1. Gcc

image-20240421163720798

image-20240421163802549

image-20240421163903356

image-20240421164151578

image-20240421164214532

image-20240421164359576

2. 静态库制作和使用

image-20240421164514336


image-20240421164609215

image-20240421164629772

  • cp calc library ../lession06 -r 将当前文件夹lession05下的calclibrary文件复制到上一级目录下的lession05

    nowcoder@nowcoder:~/Linux/lession06$ tree
    .
    ├── calc
    │ ├── add.c
    │ ├── add.o
    │ ├── div.c
    │ ├── div.o
    │ ├── head.h
    │ ├── libcalc.a
    │ ├── main.c
    │ ├── mult.c
    │ ├── mult.o
    │ ├── sub.c
    │ └── sub.o
    └── library
    ├── app
    ├── include
    │ └── head.h
    ├── lib
    │ └── libcalc.a
    ├── main.c
    └── src
    ├── add.c
    ├── div.c
    ├── mult.c
    └── sub.c

    5 directories, 19 files
    nowcoder@nowcoder:~/Linux/lession06$ cd calc
    nowcoder@nowcoder:~/Linux/lession06/calc$ rm *.o libcalc.a 删除calc下的.o文件和库文件

3. 制作动态库

  • 1.制作动态库gcc -c -fpic add.c div.c sub.c mult.c

    gcc -shared *.o -o libcalc.so

    image-20240421165213230

    image-20240421165240002

image-20240421165305089

image-20240421165527751

    1. ldd +可执行文件(查看动态库地址加载情况)

      env查看环境变量

      nowcoder@nowcoder:~/Linux/lession06/library$ ll
      总用量 36
      drwxrwxr-x 5 nowcoder nowcoder 4096 413 10:52 ./
      drwxrwxr-x 4 nowcoder nowcoder 4096 413 10:16 ../
      drwxrwxr-x 2 nowcoder nowcoder 4096 413 10:16 include/
      drwxrwxr-x 2 nowcoder nowcoder 4096 413 10:43 lib/
      -rwxrwxr-x 1 nowcoder nowcoder 8424 413 10:52 main*
      -rw-rw-r-- 1 nowcoder nowcoder 306 413 10:16 main.c
      drwxrwxr-x 2 nowcoder nowcoder 4096 413 10:16 src/
      nowcoder@nowcoder:~/Linux/lession06/library$ ldd main
      linux-vdso.so.1 (0x00007fffa132c000)
      libcalc.so => not found //<-看这里
      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f986a129000)
      /lib64/ld-linux-x86-64.so.2 (0x00007f986a71c000)

3.1export添加环境变量(临时)

image-20240421165835484

pwd查看当前文件夹所在路径

nowcoder@nowcoder:~/Linux/lession06/library$ cd lib
nowcoder@nowcoder:~/Linux/lession06/library/lib$ pwd
/home/nowcoder/Linux/lession06/library/lib //这个就是路径

echo命令用于输出变量的值

echo $LD_LIBRARY_PATH:/home/nowcoder/Linux/lession06/library/lib

3.2添加换变量(长期)

image-20240421170419092

image-20240421170443780

image-20240421170507456

image-20240421170525234

3.3动静态库的优缺点

image-20240421170617522

image-20240421170641672


4. Makefile

4.1 什么是Makefile

  • 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile 文件就像一个 shel1 脚本一样,也可以执行操作系统的命令。Makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令比如Delphi的make,VisualC++的nmake,Linux下GNU 的 make。

4.2 Makefile 文件命名和规则文件命名

  • 文件命名
    makefile 或者Makefile
  • Makefile 规则
    • 一个Makefile 文件中可以有一个或者多个规则
      • 目标… : 依赖…
        命令(shell 命令)
        目标:最终要生成的文件(伪目标除外)
        依赖:生成目标所需要的文件或是目标
        命令:通过执行命令对依赖操作生成目标(命令前必须Tab 缩进)口
      • Makefile 中的其它规则一般都是为第一条规则服务的。

4.3 工作原理

  • 命令在执行之前,需要先检查规则中的依赖是否存在

    • 如果存在,执行命令
    • 如果不存在,向不检查其它的规则,检查有没有一个规则是用来生成这个依赖的如果找到了,则执行该规则中的命令
  • 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间

    • 如果依赖的时间比目标的时间晚,需要重新生成目标

    • 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

4.4 自定义变量

image-20240421171831820

image-20240421172046159

4.5 模式匹配

add.o:add.c
gcc -c add.c
div.o:div.c
gcc -c div.c
sub.o:sub.c
gcc -c sub.c
mult.o:mult.c
gcc -c mult.c
main.o:main.c
gcc -c main.c
%.o:%.c
-%:通配符,匹配一个字符串
-两个%匹配的是同一个字符串名
%.o:%.c
gcc -c $< -o $@

image-20240421172502971

4.6 函数

  • $(wildcard PATTERN…)

    • 功能: 获取指定目录下指定类型的文件列表

    • 参数: PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔

    • 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔

    • 示例:
      $(wildcard .c ./sub/.c)

      • 返回值格式: a.c b.c c.c d.c e.c f.c

        • ```C
          #定义变量
          #获取指定目录下的.o文件 sub.o add.o mult.o div.o main.o
          src=$(wildcard./*.c)
          target=app
          $(target):$(src)
          $(CC) $(src) -o $(target)
          %.o:%.c
          $(CC) -c $< -o $@

          * $(patsubst<pattern>,<replacement>,<text>

          * 功能: 查找<text>中的单词(单词以“空格”、“Tab"或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。

          * <pattern>可以包括通配符`%`,表示任意长度的字串。如果<replacement>中也包含`%`,那么,<replacement>中的这个`%`将是<pattern>中的那个`%`所代表的字串。(可以用`\`来转义,以`\%`来表示真实含义的`%`字符)

          * 返回: 函数返回被替换过后的字符串

          * 示例:

          * `$(patsubst %.c,%.o,x.c bar.c)`

          * 返回值格式:`x.o bar.o`

          * ```
          #获取指定目录下的.o文件 sub.o add.o mult.o div.o main.o
          src=$(wildcard./*.c)
          objs=$(patsubst %.c,%.o,$(src))
          target=app
          $(target):$(src)
          $(CC) $(src) -o $(target)
          %.o:%.c
          $(CC) -c $< -o $@

        image-20240421175808645

      • touch 文件名——创建文件

      .PHONY: 文件名O——生成伪目标文件O

      • image-20240421175837436

5. GBD调试

linux命令 ll信息详解_ll命令-CSDN博客

5.1 什么是 GDB

  • GDB 是由 GNU 软件系统社区提供的调试工具,同GCC配套组成了一套完整的开发环境,GDB是Linux和许多类Unix系统中的标准开发环境。
  • 一般来说,GDB 主要帮助你完成下面四个方面的功能:
    • 1.启动程序,可以按照自定义的要求随心所欲的运行程序
      2.可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
      3.当程序被停住时,可以检查此时程序中所发生的事
      可以改变程序,将一个 BUG 产生的影响修正从而测试其他BUG4.

5.2 准备工作

  • 通常,在为调试而编译时,我们会()关掉编译器的优化选项(-o),并打开调试选项(-g)。另外,-wa11在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的BUG。

  • gcc -g-Wall program.c o program

  • -g选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。

    nowcoder@nowcoder:~/Linux/lession08$ gcc test.c -o test -g
    nowcoder@nowcoder:~/Linux/lession08$ gcc test.c -o test1
    nowcoder@nowcoder:~/Linux/lession08$ ll -h
    总用量 52K
    -rwxrwxr-x 1 nowcoder nowcoder 11K 414 20:26 test*
    -rwxrwxr-x 1 nowcoder nowcoder 8.3K 414 20:26 test1*

5.3 GDB命令-启动、推出、查看代码

  • 启动和退出
    gdb 可执行程序

    quit

  • 给程序设置参数/获取设置参数
    set args 1020
    show args

  • GDB 使用帮助
    help

  • 查看当前文件代码

    list/1 (从默认位置显示)
    list/1行号 (从指定的行显示)
    list/1 函数名(从指定的函数显示)

  • 查看非当前文件代码

    list/l 文件名:行号

    list/l 文件名:函数名

  • 设置显示的行数
    show list/listsize
    set list/listsize 行数

(gdb) list
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int test(int a);
5
6 int main(int argc, char* argv[]) {
7 int a, b;
8 printf("argc = %d\n", argc);
9
10 if(argc < 3) {
(gdb) l 20
15 b = atoi(argv[2]);
16 }
17 printf("a = %d, b = %d\n", a, b);
18 printf("a + b = %d\n", a + b);
19
20 for(int i = 0; i < a; ++i) {
21 printf("i = %d\n", i);
22 // 函数调用
23 int res = test(i);
24 printf("res value: %d\n", res);
(gdb) list main
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int test(int a);
5
6 int main(int argc, char* argv[]) {
7 int a, b;
8 printf("argc = %d\n", argc);
9
10 if(argc < 3) {

5.4 GBD命令-断点操作

image-20240421181214061

(gdb) break 9
Breakpoint 1 at 0xb2c: file main.cpp, line 9.
(gdb) b main
Breakpoint 2 at 0xafa: file main.cpp, line 6.
b bubble.cpp:11
Breakpoint 3 at 0xa18: file bubble.cpp, line 11.
b bubble.cpp:bubbleSort
Breakpoint 4 at 0x9b5: file bubble.cpp, line 8.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000000b2c in main() at main.cpp:9
2 breakpoint keep y 0x0000000000000afa in main() at main.cpp:6
3 breakpoint keep y 0x0000000000000a18 in bubbleSort(int*, int)
at bubble.cpp:11
4 breakpoint keep y 0x00000000000009b5 in bubbleSort(int*, int)
at bubble.cpp:8

5.5 GBD命令-调试命令

image-20240421181324553


6. 文件I/O(针对内存而言)

6.1 标准C库IO函数

1、标准C库I/O函数与Linux系统I/O函数的区别

(1)标准C库I/O函数在读写的时候,中间有一个缓冲区,而Linux系统I/O函数没有缓冲区;如果中间有缓冲区的话在进行读写操作的时候会先存到缓冲区,再刷新到磁盘,它比直接逐条读写到磁盘效率要高。

(2)根据应用场景选择合适的I/O函数,如:再进行网络通信时就应该使用Linux系统I/O函数,因为通信更要求实时性;而在对磁盘进行读写时则选择标准C库I/O函数。

标准C库I/O函数与Linux系统I/O函数对比(通俗易懂)_标准c库io函数和linux系统io函数对比-CSDN博客

image-20240421181551255

image-20240421181628087

6.2 标准C库IO和Linux系统和IO关系

[68-文件I/O:标准C库IO函数和Linux系统IO函数对比-CSDN博客](https://blog.csdn.net/Edward_LF/article/details/124398047#:~:text=标准c库函数和linux系统函数区别: 标准c库可以跨平台;(调用了不同平台的系统API),在linux平台中,调用c库函数,底层是调用的是linux中的系统函数 linux系统I%2FO函数是没有缓冲区的,调用一次就会访问一次)

image-20240421181845943

6.3 虚拟地址空间

  • 一个进程对应一个虚拟地址空间,由CPU中的MMU内存管理映射到真实的物理地址,程序(.c、.exe)并不占用内存空间,只占用磁盘空间。进程占用内存。文件描述符在内核区。
  • image-20240421182111634

6.4 文件描述符

image-20240421182153263

6.5 open打开文件

/*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 这三个设置是互斥的

返回值:返回一个新的文件描述符,如果调用失败,返回-1

errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号

#include <stdio.h>

void perror(const char *s);作用:打印errno对应的错误描述
参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)
// 创建一个新的文件</font>
int open(c/usr/bin/gcconst char *pathname, int flags, mode_t mode);</font>
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 打开一个文件
int fd = open("a.txt", O_RDONLY);
if(fd == -1) {
perror("open");
}
// 读写操作
// 关闭
close(fd);
return 0;
}

6.6 open创建新文件

/*open创建新文件

\#include <sys/types.h>

\#include <sys/stat.h>

\#include <fcntl.h>

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

参数:

\- pathname:要创建的文件的路径

\- flags:对文件的操作权限和其他的设置

\- 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的

\- 可选项:O_CREAT 文件不存在,创建新文件
\- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775

最终的权限是:mode & ~umask

0777 -> 111111111

& 0775 -> 111111101

\----------------------------

111111101

按位与:0和任何数都为0

umask的作用就是抹去某些权限。

flags参数是一个int类型的数据,占4个字节,32位。

flags 32个位,每一位就是一个标志位。

*/

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个新的文件
int fd = open("create.txt", O_RDWR | O_CREAT, 0777);| O_CREAT创建文件的新标志
if(fd == -1) {
perror("open");
}
// 关闭
close(fd);
return 0;
}

6.5 Linux系统i/o函数

image-20240421201504240

  • read函数读取数据是指从文件中读取数据到内存中

  • write函数写数据是指把内存中数据写到文件中

/*  
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:需要读取数据存放的地方,数组的地址(传出参数)
- count:指定的数组的大小
返回值:
- 成功:
>0: 返回实际的读取到的字节数
=0:文件已经读取完了
- 失败:-1 ,并且设置errno
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:要往磁盘写入的数据,数组
- count:要写的数据的实际的大小
返回值:
成功:实际写入的字节数
失败:返回-1,并设置errno
*/
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 1.通过open打开english.txt文件
int srcfd = open("english.txt", O_RDONLY);
if(srcfd == -1) {
perror("open");
return -1;
}
// 2.创建一个新的文件(拷贝文件)
int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664);
if(destfd == -1) {
perror("open");
return -1;
}
// 3.频繁的读写操作
char buf[1024] = {0};
int len = 0;
while((len = read(srcfd, buf, sizeof(buf))) > 0) {
write(destfd, buf, len);
}
// 4.关闭文件
close(destfd);
close(srcfd);

return 0;
}

6.6lseek函数

——移动文件指针到文件头、获取当前文件指针的位置、获取文件长度、拓展文件的长度

/*  
标准C库的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:
SEEK_SET
设置文件指针的偏移量
SEEK_CUR
设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置


作用:
1.移动文件指针到文件头
lseek(fd, 0, SEEK_SET);

2.获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);

3.获取文件长度
lseek(fd, 0, SEEK_END);

4.拓展文件的长度,当前文件10b, 110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
注意:需要写一次数据

*/

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

int main() {

int fd = open("hello.txt", O_RDWR);

if(fd == -1) {
perror("open");
return -1;
}

// 扩展文件的长度
int ret = lseek(fd, 100, SEEK_END);
if(ret == -1) {
perror("lseek");
return -1;
}

// 写入一个空数据
write(fd, " ", 1);

// 关闭文件
close(fd);

return 0;
}

6.6 lstat函数和stat函数

——获取一个文件相关的一些信息

//stat 结构体
struct stat{
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // 节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //设备文件的设备编号
off_t st_size; //文件字节数(文件大小)
blksize_t st_blksize; //块大小
blkcnt_t st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息
参数:
-pathname:操作文件的路径
-statbuf:结构体变量,传出参数,用于保存获取到的文件信息
返回值:
成功:返回0;
失败:返回-1,并设置errno;
int lstat(const char *pathname, struct stat *statbuf);
作用:获取软连接文件相关的一些信息
参数:
-pathname:操作文件的路径
-statbuf:结构体变量,传出参数,用于保存获取到的文件信息
返回值:
成功:返回0;
失败:返回-1,并设置errno;
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

int main() {

struct stat statbuf;

int ret = stat("a.txt", &statbuf);

if(ret == -1) {
perror("stat");
return -1;
}

printf("size: %ld\n", statbuf.st_size);


return 0;
}
  • 判断权限,应该和相应的宏与&操作;判断文件类型,将mode与掩码与&操作,再和宏进行比较image-20240421204638015

6.7模拟实现ls-l命令

——ls-l命令能够获取当前目录下文件的信息

​ ls-l xx.txt查看当前xx文件的信息

2024/04/21

6.8 文件属性操作函数

int access(const char *pathname, int mode);
int chmod(const char *filename, int mode);
int chown(const char *path, uid_t owner, gid_t group);
int truncate(const char *path, off_t length);
  1. *int access(const char pathname, int mode);

​ ——作用:判断某个文件是否具有某个权限,或者判断文件是否存在

​ ——参数:

​ - pathname: 判断的文件路径

​ - mode:

​ R_OK: 判断是否有读权限

​ W_OK: 判断是否有写权限

​ X_OK: 判断是否有执行权限

​ F_OK: 判断文件是否存在

​ 返回值:成功返回0, 失败返回-1

  1. *int chmod(const char filename, int mode_t);

​ —— 作用:修改文件的权限

​ 参数:

​ - pathname: 需要修改的文件的路径

​ - mode:需要修改的权限值,八进制的数

​ 返回值:成功返回0,失败返回-1

  1. int *chown(const char path, uid_t owner, gid_t group);

    —— 作用:修改文件的所有者或所在组

    vim /etc/passwd——显示所有的用户和id、组id

    vim /etc/group——查看当前系统所有组和id

    useradd xx——创建xx用户

    id xx——查看xxid

  2. int truncate(const char path, off_t length);

    作用:缩减或者扩展文件的尺寸至指定的大小

    ​ 参数:

    ​ - path: 需要修改的文件的路径

    ​ - length: 需要最终文件变成的大小

    ​ 返回值:

    ​ 成功返回0, 失败返回-1

    ——touch xx.xx 创建xx.x文件

    ——vim xx.x进入xx.x文件

6.9 目录操作函数

int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);
int rename(const char *oldpath, const char *newpath);
int chdir(const char *path);
char *getcwd(char *buf, size_t size);
  1. *int mkdir(const char pathname, mode_t mode);

man 2 xxx——查看Linux系统函数xx

​ —— 作用:创建一个目录

​ 参数:

​ pathname: 创建的目录的路径

​ mode: 权限,八进制的数

​ 返回值:

​ 成功返回0, 失败返回-1

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

int main() {

int ret = mkdir("aaa", 0777);

if(ret == -1) {
perror("mkdir");
return -1;
}

return 0;
}

结果:创建了aaa目录

nowcoder@nowcoder:~/Linux/lesson14$ gcc mkdir.c -o mkdir
nowcoder@nowcoder:~/Linux/lesson14$ ./mkdir
nowcoder@nowcoder:~/Linux/lesson14$ ll
总用量 36
drwxrwxr-x 3 nowcoder nowcoder 4096 4月 22 13:31 ./
drwxr-xr-x 9 nowcoder nowcoder 4096 4月 22 11:35 ../
drwxrwxr-x 2 nowcoder nowcoder 4096 4月 22 13:31 aaa/
-rw-rw-r-- 1 nowcoder nowcoder 1369 4月 22 11:35 chdir.c
-rwxrwxr-x 1 nowcoder nowcoder 8344 4月 22 13:31 mkdir*
-rw-rw-r-- 1 nowcoder nowcoder 519 4月 22 11:35 mkdir.c
-rw-rw-r-- 1 nowcoder nowcoder 246 4月 22 11:35 rename.c
  1. *int rmdir(const char pathname);

    ——作用:删除空目录

  2. **int rename(const char oldpath, const char newpath);

    ——作用:重命名

  3. *int chdir(const char path);

    ——作用:修改进程的工作目录

    比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder

    ——参数:

    ​ path : 需要修改的工作目录

  4. **char getcwd(char buf, size_t size);

​ ——作用:获取当前的工作路径

​ 参数:

​ - buf : 存储的路径,指向的是一个数组(传出参数)

​ - size: 数组的大小

​ 返回值:

​ 返回的指向的一块内存,这个数据就是第一个参数

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

int main() {

// 获取当前的工作目录
char buf[128];
getcwd(buf, sizeof(buf));
printf("当前的工作目录是:%s\n", buf);

// 修改工作目录
int ret = chdir("/home/nowcoder/Linux/lesson13");
if(ret == -1) {
perror("chdir");
return -1;
}

// 创建一个新的文件,在当前工作目录下创建
int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664);
if(fd == -1) {
perror("open");
return -1;
}

close(fd);

// 获取当前的工作目录
char buf1[128];
getcwd(buf1, sizeof(buf1));
printf("当前的工作目录是:%s\n", buf1);

return 0;
}

6.10 目录遍历函数

DIR *opendir(const char *name);			//打开目录
struct dirent *readdir(DIR *dirp); //读取目录
int closedir(DIR *dirp); //关闭目录

shell终端输入man 3 xx——查看标准C库函数

  • dirent结构体和d_type

image-20240422203357809

/*
// 打开一个目录
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
- name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流
错误返回NULL


// 读取目录中的数据,每调用一次reader就在目录流中往后读取一个目录
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 参数:dirp是opendir返回的结果
- 返回值:
struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL

// 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);

*/
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int getFileNum(const char * path);
// 读取某个目录下所有的普通文件的个数
int main(int argc, char * argv[]) {
if(argc < 2) {
printf("%s path\n", argv[0]);
return -1;
}
int num = getFileNum(argv[1]);
printf("普通文件的个数为:%d\n", num);
return 0;
}
// 用于获取目录下所有普通文件的个数
int getFileNum(const char * path) {
// 1.打开目录
DIR * dir = opendir(path);
if(dir == NULL) {
perror("opendir");
exit(0);
}
struct dirent *ptr;

// 记录普通文件的个数
int total = 0;

while((ptr = readdir(dir)) != NULL) {
// 获取名称
char * dname = ptr->d_name;
// 忽略掉. 和..
if(strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) {
continue;
}
// 判断是否是普通文件还是目录
if(ptr->d_type == DT_DIR) {
// 目录,需要继续读取这个目录
char newpath[256];
sprintf(newpath, "%s/%s", path, dname);
total += getFileNum(newpath);
}
if(ptr->d_type == DT_REG) {
// 普通文件
total++;
}
}
// 关闭目录
closedir(dir);
return total;
}

6.11 dup、dup2函数

int dup(int oldfd);
复制文件描述符
int dup2(int oldfd,int newfd);
重定向文件描述符
  1. #include <unistd.h>

    int dup(int oldfd);

​ 作用:复制一个新的文件描述符,指向同一个文件

​ fd=3, int fd1 = dup(fd),

​ fd指向的是a.txt, fd1也是指向a.txt

​ 从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符

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

int main() {

int fd = open("a.txt", O_RDWR | O_CREAT, 0664);

int fd1 = dup(fd);

if(fd1 == -1) {
perror("dup");
return -1;
}

printf("fd : %d , fd1 : %d\n", fd, fd1);

close(fd);

char * str = "hello,world";
int ret = write(fd1, str, strlen(str));
if(ret == -1) {
perror("write");
return -1;
}

close(fd1);

return 0;
}
  1. #include <unistd.h>

    int dup2(int oldfd, int newfd);

    ​ 作用:重定向文件描述符

    ​ oldfd 指向 a.txt, newfd 指向 b.txt

    ​ 调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt

    ​ oldfd 必须是一个有效的文件描述符

    ​ oldfd和newfd值相同,相当于什么都没有做

    ​ 返回值是:return the new file descriptor

    */

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

    int main() {

    int fd = open("1.txt", O_RDWR | O_CREAT, 0664);
    if(fd == -1) {
    perror("open");
    return -1;
    }

    int fd1 = open("2.txt", O_RDWR | O_CREAT, 0664);
    if(fd1 == -1) {
    perror("open");
    return -1;
    }

    printf("fd : %d, fd1 : %d\n", fd, fd1);

    int fd2 = dup2(fd, fd1);//fd2==fd1
    if(fd2 == -1) {
    perror("dup2");
    return -1;
    }

    // 通过fd1去写数据,实际操作的是1.txt,而不是2.txt
    char * str = "hello, dup2";
    int len = write(fd1, str, strlen(str));

    if(len == -1) {
    perror("write");
    return -1;
    }

    printf("fd : %d, fd1 : %d, fd2 : %d\n", fd, fd1, fd2);

    close(fd);
    close(fd1);

    return 0;
    }

6.12 fcntl函数

  1. **int fcntl ( int fd, int cmd, …/arg / );
    复制文件描述符
    设置/获取文件的状态标志
/*

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
int ret = fcntl(fd, F_DUPFD);

- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和我们通过open函数传递的flag是一个东西。

- F_SETFL : 设置文件描述符文件状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选性:O_APPEND, O)NONBLOCK
O_APPEND 表示追加数据
NONBLOK 设置成非阻塞

阻塞和非阻塞:描述的是函数调用的行为。
*/

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

int main() {

// 1.复制文件描述符
// int fd = open("1.txt", O_RDONLY);
// int ret = fcntl(fd, F_DUPFD);

// 2.修改或者获取文件状态flag
int fd = open("1.txt", O_RDWR);
if(fd == -1) {
perror("open");
return -1;
}

// 获取文件描述符状态flag
int flag = fcntl(fd, F_GETFL);
if(flag == -1) {
perror("fcntl");
return -1;
}
flag |= O_APPEND; // flag = flag 按位或 O_APPEND

// 修改文件描述符状态的flag,给flag加入O_APPEND这个标记
int ret = fcntl(fd, F_SETFL, flag);
if(ret == -1) {
perror("fcntl");
return -1;
}

char * str = "nihao";
write(fd, str, strlen(str));

close(fd);

return 0;
}

7. 进程

7.1进程概述

7.1.1 程序与进程

  • 程序占用磁盘资源,但不占用CPU和内存,进程占用CPU和内存,但不占用磁盘。程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:

    • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)
    • **机器语言指令:**对程序算法进行编码。
    • **程序入口地址:**标识程序开始执行时的起始指令位置。
    • **数据:**程序文件包含的变量初始值和程序使用的字面量值(比如字符串)
    • **符号表及重定位表:**描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)
    • **共享库和动态链接信息:**程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名
    • **其他信息:**程序文件还包含许多其他信息,用以描述如何创建进程
  • 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。

7.1.2 单道、多道程序设计

  • 单道程序,即在计算机内存中只允许一个的程序运行。
  • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源。**引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。**
  • 对于一个单CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽、然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个。
  • 在多道程序设计模型中,多个进程轮流使用CPU。而当下常见CPU为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行

7.1.3 时间片

  • 时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)是操作系统分配给每个正在运行的进程微观上的一段CPU时间。事实上,虽然一台计算机通常可能有多个 CPU,但是同一个CPU 永远不可能真正地同时运行多个任务。在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行由于时间片通常很短(在inux上为5ms-800ms),用户不会感觉到。

  • 时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。

7.1.4 并行和并发

  • **并行(paralle1):**指在同一时刻,有多条指令在多个处理器上同时执行。
  • **并发(concurrency):**指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的只是把时间分成若干段,使多个进程快速交替的执行。

image-20240423204853427

  • 并发是两个队列交替使用一台咖啡机。
    并行是两个队列同时使用两台咖啡机。

    image-20240423205118283

7.1.5 进程控制块PCB/进程描述符表

  • 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux内核的进程控制块是 task struct 结构体
  • 在/usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以看 struct task struct 结构体定义。其内部成员有很多,我们只需要掌握以部分即可:
    • 进程id:系统中每个进程有唯一的id,用pidt类型表示,其实就是一个非负整数
    • 进程的状态:有就绪、运行、挂起、停止等状态
    • 进程切换时需要保存和恢复的一些CPU寄存器
    • 描述虚拟地址空间的信息
    • 描述控制终端的信息
    • 当前工作目录(Current Working Directory)
    • umask 掩码
    • 文件描述符表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话(Session)和进程组
    • 进程可以使用的资源上限(Resource Limit)

7.2 进程状态转换

Linux上的进程调度策略和算法

Linux的进程调度策略和算法是操作系统管理多任务执行的核心部分,确保CPU资源能够有效分配给多个进程。Linux采用了多种调度算法和策略,旨在平衡系统性能和响应性。下面是Linux中的主要进程调度策略和算法概述:

### 1. **完全公平调度算法 (CFS - Completely Fair Scheduler)**
CFS是Linux内核自2.6.23版本起的默认调度算法,旨在使所有任务能公平地分享CPU资源。它的核心思想是确保每个进程都能以合理的时间间隔执行,并且不会因为其他进程的长时间运行而被“饿死”。

- **工作原理**:CFS为每个进程分配一个虚拟运行时间,并根据每个进程的优先级和执行时间来决定下一个运行的进程。低优先级进程的虚拟运行时间增加得更快,高优先级进程能获得更多的CPU时间。
- **红黑树**:CFS使用红黑树(红黑树是一种自平衡的二叉搜索树)来维护所有可运行的进程,选择调度下一个进程的时间复杂度为O(log n)。

### 2. **实时调度策略**
Linux也支持实时调度,适用于对时间敏感的任务。实时调度策略分为两种:
- **SCHED_FIFO**:先进先出的实时调度策略。FIFO中的进程一旦获得CPU,除非它主动让出或被一个更高优先级的进程抢占,否则它将一直运行。
- **SCHED_RR**:轮转调度,类似于FIFO,但每个进程有时间片限制。如果时间片耗尽而进程还未完成任务,它会被重新放入队列的末尾。

### 3. **SCHED_BATCH**
该调度策略适用于不需要交互的批处理任务,优先考虑系统吞吐量而不是响应时间。SCHED_BATCH调度进程时,尽量减少对其他进程的干扰,确保批处理任务在负载不重的情况下获得更多的CPU时间。

### 4. **SCHED_IDLE**
这是最低优先级的调度策略,适用于非常低优先级的进程。只有当系统空闲时,才会运行此类进程。它常用于后台作业或对性能不敏感的任务。

### 5. **时间片轮转 (Round-Robin)**
这是经典的进程调度算法之一,适用于交互式系统。每个进程在固定的时间片内执行,时间片结束后,它将被放入队列的末尾,等待下次调度。时间片轮转策略在SCHED_RR中有所体现。

### 6. **优先级调度 (Priority Scheduling)**
在Linux中,每个进程都有一个优先级,优先级高的进程会比优先级低的进程更早获得CPU时间。Linux使用动态优先级调整机制,系统会根据进程的运行状态调整其优先级,确保交互式进程能优先响应。

### 7. **多级反馈队列调度 (Multilevel Feedback Queue)**
Linux调度器有一个基于多级反馈队列的机制。不同优先级的进程会进入不同的队列,高优先级的进程会优先调度。而低优先级进程如果长期得不到执行,系统会逐渐提高它们的优先级以避免进程饿死。

### 8. **调度类 (Scheduling Classes)**
Linux调度器是多类的,CFS和实时调度属于不同的调度类。Linux通过调度类的概念来决定使用哪种调度策略,不同的调度类有不同的调度规则:
- **实时调度类**:用于实时任务。
- **CFS调度类**:用于普通任务。
- **SCHED_IDLE调度类**:用于空闲任务。

### 总结
Linux的调度算法兼顾了实时性、响应性和吞吐量,适用于不同场景的任务。CFS作为默认的调度器,确保了系统的公平性,而SCHED_FIFO、SCHED_RR等策略则满足了实时系统的需求。Linux调度器通过引入优先级、多级反馈队列等机制,动态调整进程调度,提升系统的整体性能和用户体验。

7.2.1 进程的状态

  • 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
    • 运行态:进程占有处理器正在运行
    • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
    • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成

image-20240423212620638

image-20240423214654291

7.2.2 进程相关命令

  • 查看所有进程
    ps aux/ajx
    a: 显示终端上的所有进程,包括其他用户的进程
    u: 显示进程的详细信息
    x: 显示没有控制终端的进程
    j: 列出与作业控制相关的信息

tty——查看当前进程对应的终端

*STAT参数意义
D 不可中断 Uninterruptible(usually IO)
R 正在运行,或在队列中的进程
S(大写) 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组
  • 实时显示进程动态
    top
    可以在使用 top 命令时加上 -d 来指定显示信息更新的时间 间隔,在 top 命令执行后,可以按以下按键对显示的结果 进行排序:
    M 根据内存使用量排序
    P 根据CPU 占有率排序
    T 根据进程运行时间长短排序
    U 根据用户名来筛选进程
    K 输入指定的 PID 杀死进程
    • 杀死进程
      1. kill [-signal]pid
      2. kill -l 列出所有信号
      3. kill-SIGKILL 进程ID 强制杀死进程
      4. kill -9 进程ID 强制杀死进程
      5. killall name 根据进程名杀死进程

./xx.xxx(可执行程序) &——在后台运行程序

7.2.3进程号和相关函数

  • 每个进程都由进程号来标识,其类型为pidt(整型),进程号的范围:0~32767进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
  • 任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程对应的进程号称为父进程号(PPID)
  • 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号
  • 进程号和进程组相关函数:
    • pid t getpid(void);
    • pid t getppid(void);
    • pid t getpgid(pid t pid);

7.3 进程创建

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成G进程树结构模型。

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

返回值:
成功:子进程中返回 0,父进程中返回子进程ID

​ 失败:返回 -1

失败的两个主要原因:

  1. 当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为 EAGAIN
  2. 系统内存不足,这时errno 的值被设置为NOMEM
/*
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
函数的作用:用于创建子进程。
返回值:
fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
在父进程中返回创建的子进程的ID,
在子进程中返回0
如何区分父进程和子进程:通过fork的返回值。
在父进程中返回-1,表示创建子进程失败,并且设置errno

父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中: >0 返回的子进程的ID
子进程中: =0
2.pcb中的一些数据
当前的进程的id pid
当前的进程的父进程的id ppid
信号集

共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表

父子进程对变量是不是共享的?
- 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
- 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。

*/

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

int main() {

int num = 10;

// 创建子进程
pid_t pid = fork();

// 判断是父进程还是子进程
if(pid > 0) {
// printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);


} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());

printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}

// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}

return 0;
}

/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/

7.4 父子进程虚拟地址空间情况

image-20240424124551272

image-20240424133741867

//程序运行后的结果,父子进程中的num变量互不影响
nowcoder@nowcoder:~/Linux/leeson18$ gcc fork.c -o fork
nowcoder@nowcoder:~/Linux/leeson18$ ./fork
i am parent process, pid : 61840, ppid : 61802
parent num : 10
parent num += 10 : 20
i : 0 , pid : 61840
i am child process, pid : 61841, ppid : 61840
child num : 10
child num += 100 : 110
i : 0 , pid : 61841
i : 1 , pid : 61841
i : 1 , pid : 61840
i : 2 , pid : 61841
i : 2 , pid : 61840

7.5 父子进程关系及GBD多进程调试

7.5.1 GBD多进程调试

使用GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在fork 函数调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程
设置调试父进程或者子进程:set follow-fork-mode [parent(默认)Ichild]

设置调试模式:set detach-on-fork[on | off]`

查看调试进程:show follow-fork-mode

(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".
(gdb) set follow-fork-mode child
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".

默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为f,调试当前进程的时候,其它进程被 GDB 挂起。
查看调试的进程: info inferiors

切换当前调试的进程:inferior id

使进程脱离 GDB调试:detach inferiors id

gdb 文件名进入gdb调试

gdb l查看代码

b 行数 在某行打断点

i b查看断点信息

r运行程序

n是单步调试

c执行完剩下的代码

安装ubuntu16

7.6 exec函数族

——像C++中的函数重载,是一系列功能相同或相似的函数

7.6.1exec函数族介绍

  • 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的exeC内容,换句话说,就是在调用进程内部执行一个可执行文件。
  • exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行。

7.6.2 函数族图解

内核区维护着当前进程的一些信息,比如id、状态、

image-20240425101754446

7.6.4 exec函数族

int execl(const char *path,const char *arg,.../* (char *) NULL */);

int execlp(const char *file,const char *arg,.../* (char *)NULL */);

int execle(const char *path,const char *arg,.../*,(char *)NULL, char* const envp[] */);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execvpe(const char *file,char *const argv[], char *const envp[]);
int execve(const char *filename,char *const argv[],char *const envp[]);

1 (list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
P(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址e

which 程序名——查看程序所在目录

/* execl函数 
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /home/nowcoder/a.out 推荐使用绝对路径
./a.out hello world

- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)

- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
*/
#include <unistd.h>
#include <stdio.h>

#include <fcntl.h>

int main() {
// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();

if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
execl("hello","hello",NULL);

//execl("/bin/ps", "ps", "aux", NULL);
//perror("execl");
printf("i am child process, pid : %d\n", getpid());

}

for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}
return 0;
}
//结果
nowcoder@nowcoder:~/Linux/lesson19$ gcc execl.c -o execl
nowcoder@nowcoder:~/Linux/lesson19$ ./execl
i am parent process, pid : 68644
hello, world
i = 0, pid = 68644
i = 1, pid = 68644
i = 2, pid = 68644
/*  
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
a.out
ps

- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)

- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。


int execv(const char *path, char *const argv[]);
argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
execv("/bin/ps", argv);

int execve(const char *filename, char *const argv[], char *const envp[]);
char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};


*/
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main() {


// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();

if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
execlp("ps", "ps", "aux", NULL);

printf("i am child process, pid : %d\n", getpid());

}

for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}


return 0;
}

7.7 进程控制

7.7.1进程退出exit

image-20240425124936108

status是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。

/*
#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);

status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

printf("hello\n");
printf("world");

// exit(0);
_exit(0);

return 0;
}

7.7.2孤儿进程

  • 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)
  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地 wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。
  • 因此孤儿进程并不会有什么危害。

image-20240425133450437

7.7.3僵尸进程

  • 每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程去释放。
  • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
  • 僵尸进程不能被 kill -9 杀死。
  • 这样就会导致一个问题,如果父进程不调用wait()或 waitpid()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

治理僵尸进程:

​ 杀死父进程 kill -9 进程号

7.8 wait函数

7.8.1进程回收

  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息包括进程号、退出状态、运行时间等)
  • 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
  • wait()和 waitpid()函数的功能一样,区别在于,wait()函数会阻塞waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束
  • 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

7.8.2退出信息相关函数

WIFEXITED(status)非0,进程正常退出
WEXITSTATUS(statu)如果上宏为真,获取进程退出的状态(exit的参数)

WIFSIGNALED(status)非0,进程异常终止
WTERMSIG(status)如果上宏为真,获取使进程终止的信号编号

WIFSTOPPED(status)非0,进程处于暂停状态
WSTOPSIG(status)如果上宏为真,获取使进程暂停的信号的编号
WIFCONTINUED(status)非0,进程暂停后已经继续运行
/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

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


int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {//防止产生孙子进程
break;
}
}

if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());

// int ret = wait(NULL);
int st;
int ret = wait(&st);

if(ret == -1) { //没有子进程回收就break
break;
}

if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

printf("child die, pid = %d\n", ret);

sleep(1);
}

} else if (pid == 0){
// 子进程
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}

exit(0);
}

return 0; // exit(0)
}

7.9 waitpid函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id
= 0 : options=WNOHANG, 表示还有子进程活着
= -1 :错误,或者没有子进程了
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {
break;
}
}

if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());
sleep(1);

int st;
// int ret = waitpid(-1, &st, 0);
int ret = waitpid(-1, &st, WNOHANG);

if(ret == -1) {
break;
} else if(ret == 0) {
// 说明还有子进程存在
continue;
} else if(ret > 0) {

if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

printf("child die, pid = %d\n", ret);
}

}

} else if (pid == 0){
// 子进程
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}
exit(0);
}

return 0;
}

8 进程间通信IPC

8.1进程间通信IPC(进程与进程之间收发数据的过程)

  1. 进程间通信的方式有哪几种
  2. 进程间通信某个方式的具体原理
  • 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
  • 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC:Inter Processes Communication)。
  • 进程间通信的目的:
    • 数据传输:一个进程需要将它的数据发送给另一个进程。
    • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
    • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
      • 异步可能会带来隐私问题
    • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
  • GUI——图形用户接口,IDE——集成开发环境,API——应用程序接口

Linux进程间通信的方式

1

8.2 匿名管道概述

  • 管道也叫无名(匿名)管道,它是是UNIX系统IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
  • 统计一个目录中文件的数目命令:ls | wc -l,为了执行该命令,shell 创建了两个进程来分别执行lswc
    image-20240506222147361

💥管道的特点

  • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的、操作系统大小不一定相同。

  • 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体但不存储数据。可以按照操作文件的方式对管道进行操作。

  • 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。

  • 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。

  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 1seek()来随机的访问数据。

  • 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

    image-20240507100016172

  • 管道的数据结构一般是循环队列,通过逻辑手段实现。

  • image-20240507101939000

匿名管道的使用

//创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);

//查看管道缓冲大小命令
ulimit -a

//查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd,int name);

8.3 父子进程通过匿名管道通信

为什么匿名管道可以用于父子进程间通信

匿名管道(anonymous pipe)可以用于父子进程间通信,主要是因为它提供了一种简单的、无命名的内存缓冲区,可以在同一台计算机的进程之间传递数据。以下是一些关键原因:

  1. 继承文件描述符:当父进程创建匿名管道时,会生成两个文件描述符,一个用于读取数据(读端),一个用于写入数据(写端)。在进程创建时,子进程能够继承父进程的文件描述符,这意味着子进程可以直接使用父进程创建的管道进行读写操作。

  2. 单向数据流:管道是单向的,数据只能从写端写入,从读端读取。父进程可以写数据到管道中,子进程从管道的另一端读取数据,或者反之。这种模式非常适合在父子进程之间传递数据或消息。

  3. 进程间共享内存区域:管道在内核中维护一个缓冲区,父子进程通过管道实现了在内存中的共享访问。数据从一端写入,暂时存储在内核缓冲区中,另一端可以在需要时读取数据。这使得管道成为一种高效的进程间通信方式。

  4. 进程间同步:通过管道的阻塞特性,父子进程可以同步操作。如果一端试图从空管道中读取数据,它会阻塞,直到另一端写入数据;同样,如果一端试图向已满的管道写入数据,它会阻塞,直到有空间可用。这可以实现进程间的简单同步机制。

  5. 简单高效:匿名管道的创建和使用相对简单,操作系统直接提供了系统调用(如 pipe())来创建管道,并通过标准 I/O 函数(如 read()write())进行数据读写,不需要显式的命名或配置。

因此,匿名管道因其高效、简单和父子进程之间文件描述符继承的特性,成为进程间通信(IPC)的常用方式之一。

/*
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1

管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
*/

// 创建匿名管道
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>//fork函数子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}

// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());

// 关闭写端
close(pipefd[1]);

// 从管道的读取端读取数据
char buf[1024] = {0};
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());

// 向管道中写入数据
//char * str = "hello,i am parent";
//write(pipefd[1], str, strlen(str));
//sleep(1);
}

} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
//sleep(1);

// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
// bzero(buf, 1024);
}

}
return 0;
}

匿名管道通信案例

/*
实现 ps aux | grep xxx 父子进程间通信

子进程: ps aux, 子进程结束后,将数据发送给父进程
父进程:获取到数据,过滤
pipe()
execlp()
子进程将标准输出 stdout_fileno 重定向到管道的写端。 dup2
*/

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

int main() {

// 创建一个管道
int fd[2];
int ret = pipe(fd);

if(ret == -1) {
perror("pipe");
exit(0);
}

// 创建子进程
pid_t pid = fork();

if(pid > 0) {
// 父进程
// 关闭写端
close(fd[1]);
// 从管道中读取
char buf[1024] = {0};

int len = -1;
while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
// 过滤数据输出
printf("%s", buf);
memset(buf, 0, 1024);
}

wait(NULL);

} else if(pid == 0) {
// 子进程
// 关闭读端
close(fd[0]);

// 文件描述符的重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
} else {
perror("fork");
exit(0);
}


return 0;
}

8.4 管道的读写特点和管道设置为非阻塞

管道的读写特点:

使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端
读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,
再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止

4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时 候再次write会阻塞,
直到管道中有空位置才能再次写入数据并返回。

总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待

写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
int main() {

// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}

// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());

// 关闭写端
close(pipefd[1]);

// 从管道的读取端读取数据
char buf[1024] = {0};

int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag

while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d/n", len);
printf("parent recv : %s, pid : %d\n", buf, getpid());
memset(buf, 0, 1024);
sleep(1);
}

} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
sleep(5);
}

}
return 0;
}

8.5 有名管道

  • 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。

  • 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过FIFO 不相关的进程也能交换数据。

  • 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/0系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。

  • 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于

​ 1. FIFO 在文件系统中作为一个特殊文件存在,但FIFO 中的内容却存放在内存中。(即内核的一个缓冲区)

​ 2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。

​ 3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

有名管道的使用

  • 通过命令创建有名管道
    mkfifo 名字
  • 通过函数创建有名管道
    #include <sys/types.h>
    #include <sys/stat.h>
    *int mkfifo(const char pathname,mode_tmode);
  • 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/0 函数都可用于 fifo。如:close、read、write、unlink等
  • FIFO 严格遵循先进先出(Firstin First out),对管道及FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

/*
创建fifo文件
1.通过命令: mkfifo 名字
2.通过函数:int mkfifo(const char *pathname, mode_t mode);

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

参数:

pathname: 管道名称的路径
mode: 文件的权限 和 open 的 mode 是一样,是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号

*/

有名管道注意事项

 1. 一个为只读而打开的一个管道的进程会阻塞,直到另外一个进程为只写打开管道
 2.  一个为只写而打开的一个管道的进程会阻塞,直到另外一个进程为只读打开管道

读管道:

​ 管道中有数据,read返回实际读到的字节数

​ 管道中无数据:

​ 管道写端被全部关闭,read返回0,(相当于读到文件末尾)

​ 写端没有全部被关闭,read阻塞等待

写管道:

​ 管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)

​ 管道读端没有全部关闭:

​ 管道已经满了,write会阻塞

​ 管道没有满,write将数据写入,并返回实际写入的字节数。

image-20240509104400591

模拟发送对话
//chatA
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
int main() {
//判断有名管道是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
printf("管道不存在,创建有名管道:\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if(ret == -1) {
printf("管道不存在,创建有名管道:\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
//2. 以只写的方式打开管道1
int fdw = open("fifo1", O_WRONLY);
if(fdw == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo1成功, 等待写入...\n");
//3. 以只读的方式打开管道2
int fdr = open("fifo2", O_RDONLY);
if(fdr == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo2成功, 等待读取...\n");
char buf[128];
//4.循环的写读数据
while (1)
{
memset(buf, 0, 128);
//获取标准的写入数据,标准输入是stdin
fgets(buf, 128, stdin);
//写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1) {
perror("write");
exit(0);
}
//5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0) {
perror("read");
exit(0);
}
printf("buf: %s\n", buf);
}
//6 关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
//chatB
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
int main() {
//判断有名管道是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
printf("管道不存在,创建有名管道:\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if(ret == -1) {
printf("管道不存在,创建有名管道:\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
//2. 以只读的方式打开管道1
int fdr = open("fifo1", O_RDONLY);
if(fdr == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo1成功, 等待读取...\n");
//3. 以只写的方式打开管道2
int fdw = open("fifo2", O_WRONLY);
if(fdw == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo2成功, 等待写入...\n");
char buf[128];
//4.循环的写读数据
while (1)
{

//5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0) {
perror("read");
exit(0);
}
printf("buf: %s\n", buf);

memset(buf, 0, 128);
//获取标准的写入数据,标准输入是stdin
fgets(buf, 128, stdin);
//写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1) {
perror("write");
exit(0);
}
}
//6 关闭文件描述符
close(fdr);
close(fdw);
return 0;
}

8.6 内存映射

image-20240509132841501

//内存映射相关的系统调用
#include <sys/mman.h>
void *mmap(void *addr, size tlength, int prot, int flags, int fd, off_t offset);
int munmap(void *addr,size_t length);
/*mmap-parent-child-ipc
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: 要映射的内存的首地址,未知为NULL, 由内核指定
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek
- prot : 对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。
PROT_READ、PROT_READ|PROT_WRITE
- flags :
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。即prot的权限要小于open的权限
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不便宜。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1

int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
*/

/*
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区

2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区 (磁盘文件是相同的)
- 得到一个操作这块内存的指针
- 使用内存映射区通信

注意:内存映射区通信,是非阻塞。
*/

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

// 作业:使用内存映射实现没有关系的进程间的通信。
int main() {

// 1.打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END); // 获取文件的大小

// 2.创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}

// 3.创建子进程
pid_t pid = fork();
if(pid > 0) {
wait(NULL);
// 父进程
char buf[64];
strcpy(buf, (char *)ptr);
printf("read data : %s\n", buf);

}else if(pid == 0){
// 子进程
strcpy((char *)ptr, "nihao a, son!!!");
}

// 关闭内存映射区
munmap(ptr, size);

return 0;
}

内存映射的注意事项

1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?

​ void * ptr = mmap(…);
​ ptr++; 可以对其进行++操作
​ munmap(ptr, len); // 错误,要保存地址

2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?

​ 错误,返回MAP_FAILED
​ open()函数中的权限建议和prot参数的权限保持一致。

3.如果文件偏移量为1000会怎样?

​ 偏移量必须是4K的整数倍,返回MAP_FAILED

4.mmap什么情况下会调用失败?

- 第二个参数:length = 0
    - 第三个参数:prot
    - 只指定了写权限
    - prot PROT_READ | PROT_WRITE
      第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()

6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“XXX”);
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。

7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
4K
越界操作操作的是非法的内存 -> 段错误

// 使用内存映射实现文件拷贝的功能
/*
思路:
1.对原始的文件进行内存映射
2.创建一个新文件(拓展该文件)
3.把新文件的数据映射到内存中
4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
5.释放资源
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

// 1.对原始的文件进行内存映射
int fd = open("english.txt", O_RDWR);
if(fd == -1) {
perror("open");
exit(0);
}

// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);

// 2.创建一个新文件(拓展该文件)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1) {
perror("open");
exit(0);
}

// 对新创建的文件进行拓展
truncate("cpy.txt", len);
write(fd1, " ", 1);

// 3.分别做内存映射
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}

if(ptr1 == MAP_FAILED) {
perror("mmap");
exit(0);
}

// 内存拷贝
memcpy(ptr1, ptr, len);

// 释放资源
munmap(ptr1, len);
munmap(ptr, len);

close(fd1);
close(fd);

return 0;
}
/*
匿名映射:不需要文件实体进程一个内存映射
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <bits/mman-linux.h>

int main() {

// 1.创建匿名内存映射区
int len = 4096;
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}

// 父子进程间通信
pid_t pid = fork();

if(pid > 0) {
// 父进程
strcpy((char *) ptr, "hello, world");
wait(NULL);
}else if(pid == 0) {
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}

// 释放内存映射区
int ret = munmap(ptr, len);

if(ret == -1) {
perror("munmap");
exit(0);
}
return 0;
}

9.信号概述

9.1信号的概念

  • 信号是 inux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件

  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入ctr1+C通常会给进程发送一个中断信号。
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。
    • 系统状态变化,比如 alarm定时器到期将引起SIGALRM 信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
    • 运行 kill 命令或调用 kill 函数。
  • 使用信号的两个主要目的是

    • 让进程知道已经发生了一个特定的事情。
    • 强迫进程执行它自己代码中的信号处理程序。
  • 信号的特点

    • 简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高
    • 查看系统定义的信号列表:kill -l
    • 前 31 个信号为常规信号,其余为实时信号
  • Linux信号一览表

image-20240510213256080

image-20240510215323534

image-20240510215343866

image-20240510215530329

信号的5种默认处理动作

查看信号的详细信息: man 7 signal

信号的 5 中默认处理动作
Termk 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程

信号的几种状态:产生、未决、递达
SIGKILLSIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作

进入gdb查看错误:core-file core

9.2kill、raise、abort函数

/*  
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
- 参数:
- pid :
> 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

kill(getppid(), 9);
kill(getpid(), 9);

int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig : 要发送的信号
- 返回值:
- 成功 0
- 失败 非0
kill(getpid(), sig);

void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(), SIGABRT);
*/

9.3alarm 函数

/*
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间

- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9

alarm(100) -> 该函数是不阻塞的
*/

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

int main() {

int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 0

sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3

while(1) {
}

return 0;
}

实际的时间 = 内核时间 + 用户时间 + 消耗的时间

进行文件IO操作的时候比较浪费时间

定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。

9.4signal信号捕捉函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t  signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为:
            - 参数- signum: 要捕捉的信号
        - handler: 捕捉到信号要如何处理
            - SIG_IGN : 忽略信号
            - SIG_DFL : 使用信号默认的行为
            - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
            回调函数:
                - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                - 不是程序员调用,而是当信号产生,由内核调用
                - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
        
                - 返回值:
        成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号

SIGKILL SIGSTOP不能被捕捉,不能被忽略。

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {

// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
signal(SIGALRM, myalarm);

struct itimerval new_value;

// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;

// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;

int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");

if(ret == -1) {
perror("setitimer");
exit(0);
}

getchar();

return 0;
}

9.5信号集及其相关函数

信号集

  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
  • 在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
  • 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

image-20240513211828372

1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态

3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较

  • 阻塞信号集默认不阻塞任何的信号
  • 如果想要阻塞某些信号需要用户调用系统的API

4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

  • 如果没有阻塞,这个信号就被处理

  • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

以下信号集相关的函数都是对自定义的信号集进行操作。

int sigemptyset(sigset_t * set);

  • 功能:清空信号集中的数据,将信号集中的所有的标志位置为0:
    - set,传出参数,需要操作的信号集
    - 返回值:成功返回0, 失败返回-1

int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数
* set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1

int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号:
- 参数- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1

int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号:
- 参数- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1

int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞:
- 参数- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞,不在信号集中
-1 : 失败

sigprocmask函数

image-20240513222510830

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换):
- 参数- how : 如何对内核阻塞信号集进行处理
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中默认的阻塞信号集是mask, mask | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
成功:0
失败:-1
设置错误号:EFAULT、EINVAL

int sigpending(sigset_t *set);

  • 功能:获取内核中的未决信号集
    • set,传出参数,保存的是内核中的未决信号集中的信息。
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号

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

int main() {

// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);

// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);

int num = 0;

while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);

// 遍历前32位
for(int i = 1; i <= 31; i++) {
if(sigismember(&pendingset, i) == 1) {
printf("1");
}else if(sigismember(&pendingset, i) == 0) {
printf("0");
}else {
perror("sigismember");
exit(0);
}
}

printf("\n");
sleep(1);
if(num == 10) {
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}

}


return 0;
}

后台运行—— ./可执行文件名 &

fg指令切换到前台

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

    - 功能:检查或者改变信号的处理。信号捕捉:
            - 参数- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
        - act :捕捉到信号之后的处理动作
        - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
                - 返回值:
        成功 0
            失败 -1

​ struct sigaction {

​ // 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);1️⃣

​ // 不常用
void (*sa_sigaction)(int, siginfo_t *, void *);2️⃣

​ // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;

​ // 使用哪一个信号处理对捕捉到的信号进行处理1️⃣或者2️⃣
​ // 这个值可以是0,表示使用sa_handler1️⃣,也可以是SA_SIGINFO表示使用sa_sigaction2️⃣
int sa_flags;
​ // 被废弃掉了
void (*sa_restorer)(void);
​ };

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {

struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); // 清空临时阻塞信号集

// 注册信号捕捉
sigaction(SIGALRM, &act, NULL);

struct itimerval new_value;

// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;

// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;

int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");

if(ret == -1) {
perror("setitimer");
exit(0);
}

// getchar();
while(1);

return 0;
}

image-20240515132939739

image-20240515134207766

SIGCHLD

SIGCHLD信号产生的3个条件:

​ 1.子进程结束

​ 2.子进程暂停了

​ 3.子进程继续运行

​ 都会给父进程发送该信号,父进程默认忽略该信号。

使用SIGCHLD信号解决僵尸进程的问题。

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

void myFun(int num) {
printf("捕捉到的信号 :%d\n", num);
// 回收子进程PCB的资源
// while(1) {
// wait(NULL);
// }
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret > 0) {
printf("child die , pid = %d\n", ret);
} else if(ret == 0) {
// 说明还有子进程活着
break;
} else if(ret == -1) {
// 没有子进程
break;
}
}
}

int main() {

// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);

// 创建一些子进程
pid_t pid;
for(int i = 0; i < 20; i++) {
pid = fork();
if(pid == 0) {
break;
}
}

if(pid > 0) {
// 父进程

// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);

// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);

while(1) {
printf("parent process pid : %d\n", getpid());
sleep(2);
}
} else if( pid == 0) {
// 子进程
printf("child process pid : %d\n", getpid());
}

return 0;
}

10共享内存

共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种PC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。

共享内存使用步骤

  • Ø 调用 shmget()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符
  • Ø 使用 shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
  • Ø 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat()调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • Ø 调用 shmat()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • Ø 调用 shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

image-20240911231304097

共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非0
- size: 共享内存的大小
- shmflg: 属性
- 访问权限
- 附加属性:创建/判断共享内存是不是存在
- 创建:IPC_CREAT
- 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
IPC_CREAT | IPC_EXCL | 0664
- 返回值:
失败:-1 并设置错误号
成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:和当前的进程进行关联
- 参数:
- shmid : 共享内存的标识(ID),由shmget返回值获取
- shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
- shmflg : 对共享内存的操作
- 读 : SHM_RDONLY, 必须要有读权限
- 读写: 0
- 返回值:
成功:返回共享内存的首(起始)地址。 失败(void *) -1


int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
- 参数:
- shmid: 共享内存的ID
- cmd : 要做的操作
- IPC_STAT : 获取共享内存的当前的状态
- IPC_SET : 设置共享内存的状态
- IPC_RMID: 标记共享内存被销毁
- buf:需要设置或者获取的共享内存的属性信息
- IPC_STAT : buf存储数据
- IPC_SET : buf中需要初始化数据,设置到内核中
- IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
- pathname:指定一个存在的路径
/home/nowcoder/Linux/a.txt
/
- proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
范围 : 0-255 一般指定一个字符 'a'


问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。

共享内存操作命令

ipcs 用法
ipcs –a //打印当前系统中所有的进程间通信方式的信息
ipcs –m //打印出使用共享内存进行进程间通信的信息
ipcs - q //打印出使用消息队列进行进程间通信的信息
ipcs –s //打印出使用信号进行进程间通信的信息
ipcrm 用法
ipcrm -M shmkey //移除用shmkey创建的共享内存段
ipcrm -m shmid //移除用shmid标识的共享内存段
ipcrm -Q msgkey //移除用msqkey创建的消息队列
ipcrm -g msqid //移除用msqid标识的消息队列
ipcrm -S semkey //移除用semkey创建的信号
ipcrm -s semid //移除用semid标识的信号

image-20240911231928650

共享内存的键变为0,表示标注删除

守护进程

终端

​ 在UNIX系统中,用户通过终端登录系统后得到一个she1l进程,这个终端成为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而fork()会复制PCB 中的信息,因此由shell 进程启动的其它进程的控制终端也是这个终端。

​ 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

​ 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctr1+C会产生SIGINT信号,Ctrl+\会产生SIGQUIT信号

echo$$ 查看当前终端的进程号;

Ps aux

tty 查看当前终端设备

进程组

进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合会话是一组相关进程组的集合。进程组合会话是为支持she11作业控制而定义的抽象概念,用户通过 she11 能够交互式地在前台或后台运行命令。

进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的ID,新进程会继承其父进程所属的进程组 ID。

进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

会话

会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID会成为会话 ID。新进程会继承其父进程的会话 ID。

一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。

在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。

当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程.

进程组、会话、控制终端之间的关系

image-20240911232656332

进程组、会话操作函数

image-20240911232800532

守护进程

守护进程(Daemon Process),也就是通常说的Daemon进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d结尾的名字。

守护进程具备下列特诊:

口 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭

口 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。

Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd.web 服务器 httpd 等。

守护进程的创建步骤

  • 执行一个 fork(),之后父进程退出,子进程继续执行。
  • 子进程调用 setsid()开启一个新会话。
  • 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限修改进程的当前工作目录,通常会改为根目录(/)。
  • 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
  • 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/nu11 并使用dup2()使所有这些描述符指向这个设备。
  • 核心业务逻辑
/*
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/

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

void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘文件
time_t tm = time(NULL);
struct tm * loc = localtime(&tm);
// char buf[1024];

// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

// printf("%s\n", buf);

char * str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd ,str, strlen(str));
close(fd);
}

int main() {

// 1.创建子进程,退出父进程
pid_t pid = fork();

if(pid > 0) {
exit(0);
}

// 2.将子进程重新创建一个会话
setsid();

// 3.设置掩码
umask(022);

// 4.更改工作目录
chdir("/home/nowcoder/");

// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 6.业务逻辑

// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);

struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;

// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);

// 不让进程结束
while(1) {
sleep(10);
}

return 0;
}

线程

线程概述

​ 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)。

◼ 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。

◼ 线程是轻量级的进程(LWP: Light Weight Process),在 Linux

环境下线程的本质仍是进程。

◼ 查看指定进程的 LWP 号: ps –Lf pid

线程和进程区别

◼ 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。

◼ 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。

◼ 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。

◼ 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。

线程和进程虚拟地址空间

image-20240915114326546

线程之间共享和非共享资源

image-20240915114520792

线程操作

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

pthread_t pthread_self(void);

int pthread_equal(pthread_t t1, pthread_t t2);

void pthread_exit(void *retval);

int pthread_join(pthread_t thread, void **retval);

int pthread_detach(pthread_t thread);

int pthread_cancel(pthread_t thread);

线程属性

◼ 线程属性类型 pthread_attr_t

int pthread_attr_init(pthread_attr_t *attr);

int pthread_attr_destroy(pthread_attr_t *attr);

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int* detachstate);

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

pthread_create
/*
一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程
称之为子线程。
程序中默认只有一个进程,fork()函数调用,2进行
程序中默认只有一个线程,pthread_create()函数调用,2个线程。

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
成功:0
失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);

*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
printf("child thread...\n");
printf("arg value: %d\n", *(int *)arg);
return NULL;
}

int main() {

pthread_t tid;

int num = 10;

// 创建一个子线程
int ret = pthread_create(&tid, NULL, callback, (void *)&num);

if(ret != 0) {
char * errstr = strerror(ret);
printf("error : %s\n", errstr);
}

for(int i = 0; i < 5; i++) {
printf("%d\n", i);
}

sleep(1);

return 0; // exit(0);
}
Pthread_exit
/*

#include <pthread.h>
void pthread_exit(void *retval);
功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
参数:
retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。

pthread_t pthread_self(void);
功能:获取当前的线程的线程ID

int pthread_equal(pthread_t t1, pthread_t t2);
功能:比较两个线程ID是否相等
不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的
是使用结构体去实现的。
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>

void * callback(void * arg) {
printf("child thread id : %ld\n", pthread_self());
return NULL; // pthread_exit(NULL);
}

int main() {

// 创建一个子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);

if(ret != 0) {
char * errstr = strerror(ret);
printf("error : %s\n", errstr);
}

// 主线程
for(int i = 0; i < 5; i++) {
printf("%d\n", i);
}

printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());

// 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
pthread_exit(NULL);

printf("main thread exit\n");

return 0; // exit(0);
}
Pthread_join
/*
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
- 功能:和一个已经终止的线程进行连接
回收子线程的资源
这个函数是阻塞函数,调用一次只能回收一个子线程
一般在主线程中使用
- 参数:
- thread:需要回收的子线程的ID
- retval: 接收子线程退出时的返回值
- 返回值:
0 : 成功
非0 : 失败,返回的错误号
*/

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

int value = 10;

void * callback(void * arg) {
printf("child thread id : %ld\n", pthread_self());
// sleep(3);
// return NULL;
// int value = 10; // 局部变量
pthread_exit((void *)&value); // return (void *)&value;
}

int main() {

// 创建一个子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);

if(ret != 0) {
char * errstr = strerror(ret);
printf("error : %s\n", errstr);
}

// 主线程
for(int i = 0; i < 5; i++) {
printf("%d\n", i);
}

printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());

// 主线程调用pthread_join()回收子线程的资源
int * thread_retval;
ret = pthread_join(tid, (void **)&thread_retval);

if(ret != 0) {
char * errstr = strerror(ret);
printf("error : %s\n", errstr);
}

printf("exit data : %d\n", *thread_retval);

printf("回收子线程资源成功!\n");

// 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
pthread_exit(NULL);

return 0;
}
pthread_detach
/*
#include <pthread.h>
int pthread_detach(pthread_t thread);
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
1.不能多次分离,会产生不可预料的行为。
2.不能去连接一个已经分离的线程,会报错。
- 参数:需要分离的线程的ID
- 返回值:
成功:0
失败:返回错误号
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
printf("chid thread id : %ld\n", pthread_self());
return NULL;
}

int main() {

// 创建一个子线程
pthread_t tid;

int ret = pthread_create(&tid, NULL, callback, NULL);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error1 : %s\n", errstr);
}

// 输出主线程和子线程的id
printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());

// 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放
ret = pthread_detach(tid);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error2 : %s\n", errstr);
}

// 设置分离后,对分离的子线程进行连接 pthread_join()
// ret = pthread_join(tid, NULL);
// if(ret != 0) {
// char * errstr = strerror(ret);
// printf("error3 : %s\n", errstr);
// }

pthread_exit(NULL);

return 0;
}

pthread_cancel

/*
#include <pthread.h>
int pthread_cancel(pthread_t thread);
- 功能:取消线程(让线程终止)
取消某个线程,可以终止某个线程的运行,
但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。
取消点:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
*/

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

void * callback(void * arg) {
printf("chid thread id : %ld\n", pthread_self());
for(int i = 0; i < 5; i++) {
printf("child : %d\n", i);
}
return NULL;
}

int main() {

// 创建一个子线程
pthread_t tid;

int ret = pthread_create(&tid, NULL, callback, NULL);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error1 : %s\n", errstr);
}

// 取消线程
pthread_cancel(tid);

for(int i = 0; i < 5; i++) {
printf("%d\n", i);
}

// 输出主线程和子线程的id
printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());


pthread_exit(NULL);

return 0;
}

pthread_attr

/*
int pthread_attr_init(pthread_attr_t *attr);
- 初始化线程属性变量

int pthread_attr_destroy(pthread_attr_t *attr);
- 释放线程属性的资源

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
- 获取线程分离的状态属性

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
- 设置线程分离的状态属性
*/

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

void * callback(void * arg) {
printf("chid thread id : %ld\n", pthread_self());
return NULL;
}

int main() {

// 创建一个线程属性变量
pthread_attr_t attr;
// 初始化属性变量
pthread_attr_init(&attr);

// 设置属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

// 创建一个子线程
pthread_t tid;

int ret = pthread_create(&tid, &attr, callback, NULL);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error1 : %s\n", errstr);
}

// 获取线程的栈的大小
size_t size;
pthread_attr_getstacksize(&attr, &size);
printf("thread stack size : %ld\n", size);

// 输出主线程和子线程的id
printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());

// 释放线程属性资源
pthread_attr_destroy(&attr);

pthread_exit(NULL);

return 0;
}

线程同步

◼ 线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。

◼ 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。

◼ 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。

互斥量

◼ 为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。

◼ 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。

◼ 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问

同一资源时将采用如下协议:

  1. 针对共享资源锁定互斥量
  2. 访问共享资源
  3. 对互斥量解锁

◼ 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:

image-20240915123518451

/*
使用多线程实现买票的案例。
有3个窗口,一共是100张票。
*/

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

// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;

void * sellticket(void * arg) {
// 卖票
while(tickets > 0) {
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
}
return NULL;
}

int main() {

// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);

// 回收子线程的资源,阻塞
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);

// 设置线程分离。
// pthread_detach(tid1);
// pthread_detach(tid2);
// pthread_detach(tid3);

pthread_exit(NULL); // 退出主线程

return 0;
}

互斥量相关操作函数

◼ 互斥量的类型 pthread_mutex_t

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

/*
互斥量的类型 pthread_mutex_t
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL
- restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
pthread_mutex_t *restrict mutex = xxx;
pthread_mutex_t * mutex1 = mutex;

int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;

// 创建一个互斥量
pthread_mutex_t mutex;

void * sellticket(void * arg) {

// 卖票
while(1) {

// 加锁
pthread_mutex_lock(&mutex);

if(tickets > 0) {
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
}else {
// 解锁
pthread_mutex_unlock(&mutex);
break;
}

// 解锁
pthread_mutex_unlock(&mutex);
}



return NULL;
}

int main() {

// 初始化互斥量
pthread_mutex_init(&mutex, NULL);

// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);

// 回收子线程的资源,阻塞
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);

pthread_exit(NULL); // 退出主线程

// 释放互斥量资源
pthread_mutex_destroy(&mutex);

return 0;
}

死锁

◼ 有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。

◼ 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

死锁的几种场景:

 忘记释放锁

 重复加锁

 多线程多锁,抢占锁资源

image-20240915130413972

deadlock.c

/*
互斥量的类型 pthread_mutex_t
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL
- restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
pthread_mutex_t *restrict mutex = xxx;
pthread_mutex_t * mutex1 = mutex;

int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;

// 创建一个互斥量
pthread_mutex_t mutex;

void * sellticket(void * arg) {

// 卖票
while(1) {

// 加锁
pthread_mutex_lock(&mutex);

if(tickets > 0) {
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
}else {
// 解锁
pthread_mutex_unlock(&mutex);
break;
}

// 解锁
pthread_mutex_unlock(&mutex);
}



return NULL;
}

int main() {

// 初始化互斥量
pthread_mutex_init(&mutex, NULL);

// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);

// 回收子线程的资源,阻塞
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);

pthread_exit(NULL); // 退出主线程

// 释放互斥量资源
pthread_mutex_destroy(&mutex);

return 0;
}

deadlock1.c

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

// 创建2个互斥量
pthread_mutex_t mutex1, mutex2;

void * workA(void * arg) {

pthread_mutex_lock(&mutex1);
sleep(1);
pthread_mutex_lock(&mutex2);

printf("workA....\n");

pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}


void * workB(void * arg) {
pthread_mutex_lock(&mutex2);
sleep(1);
pthread_mutex_lock(&mutex1);

printf("workB....\n");

pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);

return NULL;
}

int main() {

// 初始化互斥量
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);

// 创建2个子线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, workA, NULL);
pthread_create(&tid2, NULL, workB, NULL);

// 回收子线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

// 释放互斥量资源
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);

return 0;
}

读写锁

◼ 当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

◼ 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

◼ 读写锁的特点:

 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。

 如果有其它线程写数据,则其它线程都不允许读、写操作。

 写是独占的,写的优先级高。

读写锁相关操作函数

◼ 读写锁的类型 pthread_rwlock_t

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

/*
读写锁的类型 pthread_rwlock_t
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

案例:8个线程操作同一个全局变量。
3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量
*/

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

// 创建一个共享数据
int num = 1;
// pthread_mutex_t mutex;
pthread_rwlock_t rwlock;

void * writeNum(void * arg) {

while(1) {
pthread_rwlock_wrlock(&rwlock);
num++;
printf("++write, tid : %ld, num : %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}

return NULL;
}

void * readNum(void * arg) {

while(1) {
pthread_rwlock_rdlock(&rwlock);
printf("===read, tid : %ld, num : %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}

return NULL;
}

int main() {

pthread_rwlock_init(&rwlock, NULL);

// 创建3个写线程,5个读线程
pthread_t wtids[3], rtids[5];
for(int i = 0; i < 3; i++) {
pthread_create(&wtids[i], NULL, writeNum, NULL);
}

for(int i = 0; i < 5; i++) {
pthread_create(&rtids[i], NULL, readNum, NULL);
}

// 设置线程分离
for(int i = 0; i < 3; i++) {
pthread_detach(wtids[i]);
}

for(int i = 0; i < 5; i++) {
pthread_detach(rtids[i]);
}

pthread_exit(NULL);

pthread_rwlock_destroy(&rwlock);

return 0;
}

生产者消费者模型


Linux网络编程

网络结构模式

C/S结构

简介

服务器 - 客户机,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器,服务器则是提供信息供人访问的计算机。

客户机通过局域网与服务器相连,接受用户的请求,并通过网络向服务器提出请求,对数据库进行操作。服务器接受客户机的请求,将数据提交给客户机,客户机将数据进行计算并将结果呈现给用户。服务器还要提供完善安全保护及对数据完整性的处理等操作,并允许多个客户机同时访问服务器,这就对服务器的硬件处理数据能力提出了很高的要求。

在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。

优点

  1. 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后再提交给服务器,所以 C/S 结构客户端响应速度快;
  2. 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求;
  3. C/S 结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程;
  4. 安全性较高,C/S 一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校验,提供了更安全的存取模式,对信息安全的控制能力很强,一般高度机密的信息系统采用 C/S 结构适宜。

缺点

  1. 客户端需要安装专用的客户端软件。首先涉及到安装的工作量,其次任何一台电脑出问题,如病毒、硬件损坏,都需要进行安装或维护。系统软件升级时,每一台客户机需要重新安装,其维护和升级成本非常高;
  2. 对客户端的操作系统一般也会有限制,不能够跨平台。

B/S结构

简介

B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件。这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。客户机上只要安装一个浏览器,如 Firefox 或 Internet Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。

优点

  1. B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。

缺点

  1. 通信开销大、系统和数据的安全性较难保障;
  2. 个性特点明显降低,无法实现具有个性化的功能要求;
  3. 协议一般是固定的:http/https;
  4. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低;