Linux系统编程

本文围绕Linux系统编程展开,介绍了Vscode远程连接Linux的方法,阐述了系统编程基本概念、程序框架,详细讲解了标准IO和文件IO,包括相关函数的使用,还涉及库的制作与使用、进程控制、通信方式(管道、信号、共享内存等)以及信号量等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0、Vscode远程连接Linux

选择第2个SSH连接

image-20240322104333775

9df3e63859dcc55dd18a20a1dd34401

依次是主机名,IP地址,用户地址

image-20240322153328918

点击打开文件夹,这样左边可以现实相应的文件夹。

image-20240322153454864

完成连接后,点第一个“文件”图标,选择文件夹

image-20240322160246738

1、什么是Linux系统编程

介于应用层和驱动层之间原因

Linux内核向应用层提供相应接口

2、Linux系统编程基本程序框架

argv 是指向字符串的指针数组

image-20240319151124137

文件名(这里是a.out)本身就是一个参数

image-20240319151005141

image-20240319151245555

image-20240319151320019

视频最后有讲开发板交叉编译

3、标准IO和文件IO

1.什么是文件IO? 文件IO就是直接调用内核提供的系统调用函数。

2.什么是标准IO? 标准IO就是间接调用系统调用函数,是C库函数。

image-20240319185944099

文件IO和标准IO的区别?

  • 文件IO是直接调用内核提供的系统调用函数,头文件是unistd.h,标准IO是间接调用系统调用函数,头文件是stdio.h。

  • 文件IO是依赖于Linux操作系统的,标准IO是不依赖操作系统的,所以在任何的操作系统下,使用标准IO,也就是C库函数操作文件的方法都是相同的。

4、文件IO之open函数

文件描述符

对于文件IO来说,一切都是围绕文件描述符来进行的。在Linux系统中,所有打开的文件都有一个对应的文件描述符。

文件描述符的本质是一个非负整数,当我们打开一个文件时,系统会给我们分配一个文件描述符。

当我们对一个文件做读写操作的时候,我们使用open函数返回的这个文件描述符会标识该文件,并将其作为参数传递给read或者write函数

在posix.1应用程序里面,文件描述符0,1,2分别对应着标准输入,标准输出,标准出错

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

第三个参数为无符号整型。当第二个参数用到O_CREAT时,就要用到。表示创建的文件的权限。

image-20240319210221235

0666会与掩码相与(掩码要取反),第一个0表示是8进制数。

image-20240319205635051

程序执行成功的话返回3。因为0 1 2被占用了。

image-20240319210646168

Linux中查看掩码: 在命令行输入umask

5、文件IO之close函数

  • 返回值:如果成功返回0,如果失败返回-1

image-20240320092757798

6、文件IO之read函数

ssize_t read(int fd, void *buf, size_t count);

  • fd:文件描述符,open得到的,通过这个文件描述符操作某个文件

  • buf:地址。存放读取到的数据

  • count:要读的数据的实际的大小

  • 返回值:如果成功大于0返回实际读取到的字节数,等于0表示文件已经读取完毕(读到了末尾);如果失败返回-1并设置errno

image-20240320094442692

image-20240320094352510

  • 这里的read返回值ret比字符多一个,是因为字符末尾的'\0'吗?改了回车还是一样的呢

  • 字符串后默认有一个\0,也算一个字符

在上面程序基础上改动

image-20240320095012329

第2次read已经读到末尾,所以返回0。

image-20240320095034511

7、文件IO之write函数

ssize_t write(int fd, void *buf, size_t count);

  • fd:文件描述符,open得到的,通过这个文件描述符操作某个文件

  • buf:地址。存放写入到的数据

  • count:要读的数据的实际的大小

  • 返回值:如果成功大于0返回实际读取到的字节数,等于0表示没有任何内容写入文件;如果失败返回-1并设置errno

屏幕就是标准输出

fd=1就是在屏幕上写数据。

image-20240320100416735

image-20240320100520319

在上面的程序基础上,取消注释,当把1改成fd时

image-20240320100636976

  • 注意:打开文件时的权限

8、综合练习1

image-20240323103254175

  • 注意:read和write一般和open配套使用。

<src file>源文件 <obj file>目标文件

第4步,判断条件是热爱的函数没有读到字符串的末尾。write写入多少个数据,就是read多少个读到的数据

  • 注意36-38行,必须在while的判断条件里才能正常读取。(跟12节readdir实验同理)

image-20240322195135173

image-20240322195146968

在命令行输入:

  • gcc test.c -o test

  • ./test a.c b.c

9、文件IO之lseek函数

lseek 函数

off_t lseek(int fd, off_t offset, int whence);

image-20240322201559779

image-20240322201643068

作用: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);

在6、文件IO之read函数的程序基础上修改,读完第一次后,使用lseek再读第二次。

image-20240322202024630

得到的结果

image-20240322202121795

再次修改程序,第一次只读2个。

image-20240322202435558

得到的结果

image-20240322202416506

10、目录IO之mkdir函数

image-20240322202517783

mkdir 函数

int mkdir(const char \*pathname, mode_t mode);

  • 作用:创建一个目录

  • 参数

    • pathname:创建的目录的路径

    • mode:权限,八进制的数

  • 返回值:如果成功返回0;如果失败返回-1并设置errno

image-20240322204211134

得到的结果

image-20240322204319622

11、目录IO之opendir和closedir函数

opendir 函数

DIR *opendir(const char *name);

  • 作用:打开一个目录

  • 参数name:需要打开的目录的名称

  • 返回值:返回 DIR* 类型的目录流

closedir 函数

int closedir(DIR *dirp);

  • 作用:关闭目录指定目录

  • 参数dirp:opendir返回的结果

  • 返回值:如果成功返回0;如果失败返回-1并设置errno

image-20240322205002115

得到的结果。

image-20240322205028599

12、目录IO之readdir函数

readdir 函数

struct dirent *readdir(DIR *dirp);

  • 作用:读取目录中的信息

  • 参数: dirp:opendir返回的结果

  • 返回值:struct dirent* 类型表示如果读取成功返回 dirent 结构体;如果读取到的文件的信息读取到了末尾或者失败了,返回NULL

结构体指针,只需关注第1个和最后一个。

image-20240322205426872

命令行输入:ls -l;文件名前的就是inode号

image-20240322205346903

在test文件夹中创建a,c文件后,ls -ali一共会显示三个文件(在内核中,文件的内容通过链表存放)(以链表结构呈现):

image-20240322212150776

执行程序:

image-20240322212424899

image-20240322212448281

得到的结果,只打印一个文件(程序中dir只读了一次):

image-20240322212312204

修改程序,用while读取:

image-20240322212642880

得到的结果:

image-20240322212703159

13、综合练习2

image-20240323103307101

综合练习是通过命令行实现,所以不需要原程序的步骤1

  • 注意:46行中必须是“/”,不能是‘/’

     fd_src=open(strcat(strcat(file_path,'/'),file_name), O_RDWR);

    程序目标:把test_tt文件夹中的a.c复制到外面来

    image-20240324180632535

程序:

image-20240324181215358

image-20240324181245153

image-20240324181302637

14 库的基本概念

1、什么是库

  • 库是一种可执行的二进制文件,是编译好的代码。

库是计算机上的一类提供直接使用的变量、函数、类的文件。 库算是一种特殊的程序但不能单独运行可以看成一种代码仓库。 库具有保密代码方便部署和分发的作用。

2、为什么要使用库

  • 提高开发效率。

3、Linux下库的种类

  • 静态库:静态库在程序编译的时候会被链接到目标代码里面。所以我们的程序运行就不在需要该静态库了。因此编译出来的体积就比较大。静态库以lib开头,以.a结尾

  • 静态库在程序移植时适用

  • 动态库:(动态库也叫共享库)动态库在程序编译的时候不会被链接到目标代码里面,而是在程序运行的时候被载入的,所以我们的程序运行就需要改动态库了。因此编译出来的体积就比较小。动态库以lib开头,以.so结尾

15静态库的制作与使用

静态库制作步骤:

  • 编写或准备库的源代码

  • 将源码.c文件编译生成.o文件

  • 使用ar命令创建静态库

  • 测试库文件

image-20240324210122261

c --create, r --trplace

image-20240324210254996

得到的结果:

image-20240324210422298

image-20240324210440121

如果我们的程序代码用到了库文件里面的函数,我们在编译的时候需要链接库。系统默认会在/lib或者/usrlib去找库文件。或者在编译的时候我们制定库的路径。

举例:

  • gcc test.c -lmylib -L

    "-l":指定静态库的库名

    '-L':指定静态库的查找位置。'-L.' :表示在当前目录下去查找

16动态库的制作与使用

动态库制作步骤:

  • 编写或准备库的源代码

  • 将源码.c文件编译生成.o文件

  • 使用gcc命令创建动态库

  • 测试库文件

这样执行不成功,显然libmylib.so不在/lib,/usr/lib这2个目录下

image-20240324212240867

在动态库使用是,系统会默认去/lib,/usr/lib目录下去查找动态函数库,如果我们使用的库不在里面,就会提示错误。解决这个问题有三种方法。

  • 第一种方法(不建议):

    将生成的动态库拷贝到/ib或者/usr/lib里面去,因为系统会默认去这俩个路径下寻找。

  • 第二种方法(只对当前终端有效):

    把我们的动态库所在的路径加到环境变量里面去,比如我们动态库所在的路径为/home/test,我们就可以这样添加:

     export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/test

    image-20240324212506490

  • 第三种方法: 修改ubuntu下的配置文件/etc/ld.so.conf,我们在这个配置文件里面加入动态库所在的位置,然后使用命令ldconfig更新目录。

    image-20240324212819996

    image-20240324212723815

image-20240324212843293

17进程的基本概念

程序就是在磁盘上的编译好的可执行二进制文件,程序准备好之后 就处在就绪态

后台进程也叫守护进程

image-20240324214635718

AB同学之间的交流就是进程之间的通信,要通过内核才能通信,要用文件IO

1.什么是进程? 进程指的是正在运行的程序

2.进程ID 每个进程都有一个唯一的标识符,即进程ID,简称pid 3.进程间的通信的几种方法?

管道通信:有名管道,无名管道 信号通信:信号的发送,信号的接受,信号的处理 IPC通信:共享内存,消息队列,信号灯Socket通信

4.进程的三种基本状态以及转换

18进程控制

fork 函数

pid_t fork(void);

  • 作用:用于创建一个新的进程(子进程)

  • 返回值:fork函数有三种返回值,在父进程中,fork返回新创建的子进程的PID,在子进程中,fork返回0,如果出现错误,fork返回一个负值。

父子进程:** **

相关函数

 pid_t getpid(void);

作用:用于获取调用进程的进程号 返回值:如果成功子进程中返回 0,父进程中返回子进程 ID调用进程的进程号;如果失败返回-1并设置errno

 pid_t getppid(void);

作用:用于获取调用进程的父进程号 返回值:如果成功子进程中返回 0调用进程的父进程号;如果失败返回-1并设置errno

 pid_t getpgid(void);

作用:用于获取调用进程的进程组号 返回值:如果成功子进程中返回进程组号;如果失败返回-1并设置errno

image-20240325094826992

image-20240325094905429

得到的结果,父进程和子进程都会打印!!

image-20240325094937714

父进程和子进程关系:子进程相当于父进程的拷贝。

代码执行位置:

  • 父进程从最开始的地方开始执行

  • 子进程从fork()函数之后开始执行

代码执行顺序:

  • 谁先抢到CPU顺序谁先执行

image-20240325095516575

  1. 标准 C 库函数 exit()

  2. 标准 Linux 系统库函数 _exit()

image-20240326212820127

注意:exit() 在调用 _exit() 之前会进行刷新I/O缓冲,由于当 std::endl 或者 \n 被输出时,缓冲区会被刷新,所以数据会被立即显示在屏幕上,而直接调用 _exit() 不会进行刷新I/O缓冲,所以当 std::endl 或者 \n 未被输出时数据不会被显示在屏幕上。

19exec函数族

什么是 exec 函数族

在调用进程内部执行一个可执行文件。

有的情况下不想让子进程和父进程执行想的的代码,这时就要用到exec函数族

在Linux中并没有exec函数,而是有6个以exec开头的函数族,下面列举了exec函数族的6个函数成员的函数原型。

 #include <unistd.h>
 extern char *environ;
 ​
 最常用:int execl(const char *path, const char *arg, ...);
 int execlp(const char *file, const char *arg, ...);
 int execle(const char *path, const char *arg,..., char * const envp[]);
 int execv(const char *path, char *const argv[]);
 int execvp(const char *file, char *const argv[]);
 int execvpe(const char *file, char *const argv[],char *const envp[]);
 ​

exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段、据段和堆栈等都已经被新的内容取代,只留下进程ID 等一些表面上的信息仍保持原样;只有调用失败了才会返回 -1并从原程序的调用点接着往下执行。

调用exec 函数族并不是新建一个进程而是只替换用户区的数据

换核不换壳:当进程调用exec函数时,进程的用户空间和数据空间会被新的程序完全替代

image-20240325101857864

在Linux中使用exec函数族主要有以下两种情况:

  • 1.当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 所数族让自己重生换核不换壳

  • 2.如果一个进程想执行另一个程序,那么它就可以调用fork函数新建个进程,然后调用任何一个exec函数使子进程重生。

exit(0):执行成功。exit(1):执行失败

如果execl()函数执行成功,则他之后的程序都不会执行。

image-20240325103252044

image-20240325103220794

得到的结果:

image-20240325103111774

修改子进程代码:

image-20240325104607418

得到的结果,和在命令行输入ls -al的结果是一样的:

image-20240325104550217

20ps和kill命令

ps命令

  • ps命令可以列出系统中当前运行的那些进程

  • 命令格式:ps [参数]

  • 命令功能:用来显示当前进程的状态

  • 常用参数:aux

pid号,TTY表示当前进程要关联终端。状态。启动时间。具体执行程序

image-20240326211315101

内存相关使用情况:VSZ虚拟内存使用大小。RSS实际内存使用。

image-20240326211403189

TTY不一样,表示不关联终端的状态。

image-20240326211700061

加上“|”后,ps aux的所有信息会给到内存里,grep会在ps aux中查找到我们想要的信息。

image-20240326211900813

kil命令

  • 用来杀死进程

  • 举例:kill -9(SIGKILL)PID

程序:

image-20240326212429523

image-20240326212451470

得到的结果:(为什么grep显示的信息都是2条???)

image-20240326212527808

所有的kill:

image-20240326212213349

21孤儿进程和僵尸进程

孤儿进程

父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process) 。

孤儿进程没有什么危害,已领养孤儿进程的父进程(内核的 init 进程)会循环地 wait() 已经退出的子进程,最终会处理子进程直到其结束生命周期。

孤儿进程会被init进程(pid号为1的进程)领养。注意sleep(2)

image-20240326213704274

Ubuntu用upstart作为默认的init进程:

image-20240326213813282

僵尸进程 子进程结束以后,父进程还在运行,但是父进程不去释放进程控制块这个子进程就叫做僵尸进程。

每个进程结束之后,都会释放自己地址空间中的用户区数据, 内核区的 PCB没有办法自己释放掉,需要父进程去释放,(所以孤儿进程会被领养!!)。进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程(Zombie Process)。

image-20240326214538056

Z+表示僵尸状态。defunct表示进程死掉了

image-20240326214715934

22wait函数

进程回收 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。 父进程可以通过调用 wait() 或 waitpid() 得到它的退出状态同时彻底清除掉这个进程。

头文件:#include <sys/wait.h>

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

返回值:成功返回回收的子进程的pid,失败返回-1

与wait函数的参数有关的俩个宏定义:

  • WIFEXITED(status):如果子进程正常退出,则该宏定义为真

  • WEXITSTATUS(status):如果子进程正常退出,则该宏定义的值为子进程的退出值。

wait函数会阻塞在这里,直到 子进程退出。如果有子进程退出,wait会自动收集其信息并销毁该进程。33行将子进程的退出值设成了6. exit(6)表示正常退出,不是被某个进程干掉了

image-20240327092728684

得到的结果:

image-20240327093129902

23守护进程

1.什么是守护进程?

守护进程运行在后台,不跟任何控制终端关联。守护进行在后台运行,支撑当前的一些进程。

2.怎么创建一个守护进程?

有俩个基本要求:1,必须作为我们init进程的子进程 2.不跟控制终端交互。

  1. 使用fork函数创建一个新的进程,然后让父进程使用exit函数直接退出(必须要的)

  2. 调用setsid函数。(必须要的)

  3. 调用chdir函数,将当前的工作目录改成根目录,增强程序的健壮性。(不是必须要的)

  4. 重设我们umask文件掩码,增强程序的健壮性和灵活性(不是必须要的)

  5. 关闭文件描述

为什么调用setsid()函数?

  • 摆脱原来进程组的控制,因为子进程复制了父进程的所有信息,包括会话,进程组,控制终端

  • 调用setsid()函数会创建一个新的会话,调用的进程就是新建会话的唯一进程,但是没有控制终端。也就是调用setsid函数 抛弃控制终端

5 要关闭哪些文件描述符?012标准输入、输出、字符串。是为了节省资源

会话和进程组和进程的关系

image-20240327093915380

image-20240327094905172

image-20240327094809587

得到的结果:a.out是守护进程,一直在后台运行。

image-20240327095043662

24管道通信之无名管道

int pipe(int pipefd[2]);

其中参数是得到的文件描述符,不是传进去的!!!

  • 作用:创建一个匿名管道,用来进程间通信

  • 参数

     int pipefd[2]

    这个数组是一个传出参数

    • pipefd[0] :对应的是管道的读端

    • pipefd[1] :对应的是管道的写端

  • 返回值:如果成功返回 0;如果失败返回 -1

image-20240327095134609

无名和有名区别:文件系统里有没有文件名。

无名管道只能实现有亲缘关系间进程的通信。因此pipe()函数必须在fork()函数之前,子进程能继承父进程的文件描述符!!!

程序:如果没有35行,父进程没有往管道里写数据,子进程就没数据可读,就会一直阻塞。

image-20240327130726510

image-20240327130617994

得到的结果:第一次没有35行程序,就一直阻塞。第二次有35行程序,正常运行。

image-20240327130645348

25管道通信之有名管道

  • 函数创建

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

    • 作用:创建FIFO文件

    • 参数

      • pathname:管道名称的路径

      • mode:文件的权限(和 open()mode 是一样的)

  • 命令创建

    一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open() 打开它,常见的文件 I/0 函数都可用于 FIFO。如: close()read()write()unlink()

 mkfifo filename

管道和块设备文件一样,不占用磁盘空间

image-20240327152011037

写数据,access判断当前文件是否存在

image-20240327152252652

image-20240327152346891

读数据:

image-20240327152658247

image-20240327152744269

第一次执行read,和无名管道一样,无数据可读,被阻塞:

image-20240327152850295

当前文件夹下存在管道fifo,故不会创建新的管道,此时read开始读取到数据:

image-20240327153037484

image-20240327153139160

当把fifo删了之后,课创建新的管道:

image-20240327153246055

信号概念

什么是信号

  • 信号是 Linux 进程间通信是事件发生时对进程的通知机制,有时也称之为软件中断。

  • 信号是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。

  • 信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

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

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号比如输入Ctrl+C通常会给进程发送一个中断信号。

  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。

  • 系统状态变化。比如 alarm() 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。

  • 运行 kill 命令或调用 kill()

26信号通信(一)信号发送

进程间通信方式IPC包裹共享内存和消息队列。

进程之间不能直接发送信号

image-20240327155437512

分为三个步骤:

  • 信号发送

  • 信号接收

  • 信号处理

信号发送函数

int kill(pid t pid, int sig);

  • 作用:给任何的进程或者进程组 pid,发送任何的信号 sig

  • 参数:

  • pid:

    大于0:将信号发送给指定的进程

    = 0:将信号发送给当前的进程组

    = -1:将信号发送给每一个有权限接收这个信号的进程

    <-1:这个pid=某个进程组的ID取反 (如:-12345)

    sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

    返回值:如果成功返回0;如果失败返回-1,并设置errno

int raise(int sig);

  • 作用:给当前进程发送信号(给自己发信号

  • 参数: sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

  • 返回值:如果成功返回0;如果失败返回-1,并设置errno

raise();函数等价于kill(getpid(), sig);

void abort(void);

  • 作用:发送SIGABRT信号给当前的进程,杀死当前进程

程序:

image-20240327161730231

得到的结果:

image-20240327161757471

程序:kill.c发送命令把test.c杀死

image-20240327161940028

image-20240327162005842

得到的程序:

image-20240327162145760

alarm 函数(闹钟函数)

unsigned int alarm(unsigned int seconds);

  • 作用:设置定时器(闹钟) 。函数调用,开始倒计时,当倒计时为8的时候函数会给当前的进程发送一个信号: SIGALARM

  • 参数:seconds:倒计时的时长,单位: 秒。如果参数为0,定时器无效(不进行倒计时,不发信号),如取消一个定时器:alam(0)

  • 返回值:如果之前没有计时器,返回0;如果之前有计时器返回之前计时器剩余的时间

SIGALARM : 默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。

闹钟设置3秒,3秒后进程没有捕捉信号,就会把进程终止掉(3秒后信号的默认动作是终止进程):

image-20240327162412429

得到的结果:

image-20240327162539987

27信号通信(二)信号接收

接收信号:如果要让我们接收信号的进程可以接收到信号,那么这个进程就不能停止。

如何让信号不停止:三种方法

  • while()

  • sleep()

  • pause()

头文件:#include <unistd.h>

定义函数:int pause(void);

函数说明:pause()会令目前的进程暂停(进入睡眠状态),直到被信号(signal)所中断.

返回值:只返回-1

程序和结果:pause.c处于睡眠状态(S+)。

image-20240327165018453

在键盘上按CRT了+C之后,进程就终止了:

image-20240327165129483

28信号通信(三)信号处理

信号处理的三种方式:系统默认(大部分系统默认终止进程),忽略,捕获。

typedef void(*sighandler_t)(int)

sighandler_t signal(int signum, sighandler_t handler);

  • 作用:设置某个信号的捕捉行为

  • 参数:

    signum:要捕捉的信号

    handler:捕捉到信号要如何处理

    • SIG_IGN:忽略信号

    • SIG_DFL:使用信号默认的行为

    • 回调函数:这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号

  • 返回值:如果成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL;如果失败返回SIG_ERR,设置errno

可以简化成: signal(参数1,参数2)

  • 参数1:我们要进行处理的信号,系统的信号我们可以再终端键入 kil -l查看

  • 参数2:处理的方式(是系统默认还是忽略还是捕获

注意:SIGKILL、SIGSTOP不能被捕捉,不能被忽略。 回调函数需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义,不是程序员调用,而是当信号产生,由内核调用。函数指针是实现回调的函数实现之后,将函数名放到函数指针的位置就可以了。

三类处理方式:

image-20240327195146926

第一种方式:忽略。此时Crtl+C和kill -2 pid号命令无法杀死进程。

image-20240327200446438

得到的结果:

image-20240327200629033

kill -9 pid号命令才能杀死:

image-20240327200720064

第二种方式:系统默认。

image-20240327200836213

得到结果:

image-20240327200906223

第三种方式:回调函数中的形参sig和signal()函数中的第一个参数对应。

image-20240327201106245

得到结果:此时键盘Crtl+C会显示get signal。

image-20240327201134204

注意:如果发的不是SIGINT信号,其他信号是不会忽略或者处理的

29共享内存

什么是共享内存

共享内存是一种用于多进程或多线程之间共享数据的机制,它允许不同的进程或线程在物理内存创建一个共享区域(段)。

由于一个共享内存段会称为一个进程用户空间的一部分,因此这种进程间通信(IPC) 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

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

共享内存使用步骤

  1. 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。

  2. 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。

  3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。

  4. 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。

  5. 调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存才会销毁。只有—个进程需要执行这一步

创建共享内存

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

  • 作用:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识

    新创建的内存段中的数据都会被初始化为0

  • 参数:

    • key:IPC_PRIVATE(是一个宏)或者是ftok函数的返回值

    • size:共享内存的大小

    • shmflg:权限

      • 当不是用宏创建时需要用到一下附加属性:

        1. 创建:IPC_CREAT

        2. 判断共享内存是否存在:IPC_EXCL,需要和 IPC_CREAT 一起使用 IPC_CREAT | IPC_EXCL | 0664

  • 返回值:成功返回共享内存的标识符,失败返回-1

当使用 IPC_PRIVATE 时,内核会创建一个新的唯一标识符,该标识符仅在调用进程及其子进程之间有效,而不会在系统范围内注册。

程序:

image-20240327204743258

得到结果:通过IPC_PRIVATE创建的共享内寸的key是0.

image-20240327204820781

可通过ipcs -m shmid命令来删除该共享内存。

image-20240327205059374

key_t ftok(const char *pathname, int proj_id);

  • 作用:根据指定的路径名,和 int 值,生成一个共享内存的key

  • 参数:

    • pathname:文件路径及文件名

    • proj_id: 字符

  • 返回值:成功返回key值,失败返回-1

image-20240327205500552

得到结果:产生的key值不是0。

image-20240327205751757

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

  • 作用:将共享内存映射到用户空间,这样用户操作时就不用进入共享内存,直接在用户空间内操作就行。所以说共享内存是进程间通信效率最高的方法。

    image-20240327210433665

  • 参数:

    • shmid:共享内存的标识(ID),也是shmget 函数的返回值

    • shmaddr:映射到的地址,一般写NULL,NULL为系统自动帮我完成映射

    • shmflg:对共享内存的操作,通常为0

      • 只读:SHM_RDONLY

      • 读写:0

  • 返回值:成功返回共享内存映射到进程中的地址,失败返回-1

int shmdt(const void \*shmaddr);

  • 作用:将进程里的地址映射删掉

  • 参数:shmaddr:共享内存映射后的地址

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

  • 注意:shmdt函数是将进程中的地址映射删除,也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离并不会删除内核里面的共享内存对象。

    删掉的是红色箭头指的这块。上面这块

    a7dde6597aff061274616aa042aace6

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • 作用:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共

  • 参数:

    • shmid:要删除的共享内存的标识符(ID)

    • cmd:要做的操作

      • PC_STAT:获取共享内存的状态;

      • IPC_SET:设置共享内存的状态;

      • IPC_RMID:删除共享内存

    • buf:与第二个参数配对

      • IPC_STAT:保存获取到的共享内存的状态

      • IPC_SET:设置需要设置的共享内存的状态;

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

    删掉的是黄色箭头指的这块。下面这块

    a7dde6597aff061274616aa042aace6

使用共享内存完成父进程和子进程间的通信:子进程中的sleep(2)能保证父进程先运行。

image-20240328184324243

image-20240328184231164

得到结果:

image-20240328184419078

30消息队列

31信号量

信号量本质:计数器

保护共享资源,当减到0时,就不能使用共享资源了。

semget函数:获得信号量的ID

image-20240328201726924

函数原型:int semget(key_t key, int nsems, int semflg);

  • 函数功能:获得信号量的ID

  • 参数:

    • key_t key:信号量的键值。

    • int nsems:信号量的数量

    • int semflg:标识,权限

  • 返回值:成功返回信号量的ID,失败返回-1

semctl函数:对信号量操作,由cmd决定

函数原型:int semctl(int semid, int semnum, int cmd, union semun arg)

  • 参数:intsemid:信号量ID,

  • intsemnum:信号量编号

  • cmd:

    • IPC_STAT(获取信号量的属性)

    • IPC_SET(设置信号量的属性)

    • IPC_RMID(删除信号量)

    • SETVAL(设置信号量的值)

  • arg:union semun{

    int val;

    struct semid ds *buf;

    unsigned short *array,

    struct seminfo *buf; }

semop函数:对信号量加1减1操作

int semop(int semid, struct sembuf *sops, size_t nsops);

参数:

  • int semid信号量ID,

  • struct sembuf*sops 信号量结构体数组

  • size_t nsops 要操作信号量的数量

struct sembuff{

unsigned short sem_num; //要操作的信号量的编号

short sem_op; //PN操作,1为V操作,释放资源。-1为P操作,分配资源。0为 等待,直到信号量的值变成0

short sem_flg; //0表示阻塞,IPC NOWAIT表示非阻塞

}

程序,尽管子进程中sleep,但想让子进程先执行。初始信号量为0,父进程中操作为-1,所以会阻塞,等子进程+1之后,父进程才能-1,然后要把信号量恢复。

image-20240328205925127

image-20240328210722259

!

image-20240328210145412

image-20240328210328863

得到结果:子进程先运行。

image-20240328210404545

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值