[cpp]Linux系统编程(到进程)

gcc的使用

静态库制作

  1. 将c文件编译(.obj)、汇编(.o)为二进制文件gcc -c file.c
  2. 使用ar工具,将二进制文件(.o)打包为一个库(命名方式:libxxxx.a)ar rcs libxxx.a xxx.o
  3. 此时生成了一个只含有函数定义的文件,但是他们的入口并没有被包括(.h头文件)
  • 对于一个引用了此库的文件,首先要包含头文件(-I include的头文件所在位置) 其次要包含库文件(-l 库文件名称 -L 库文件所在目录)
  • 这样就形成了一条编译语句:gcc main.c -o app -I ../include/ -l calc -L ../lib/

动态库制作

需要获得和位置无关的代码:
gcc -c -fpic/-fPIC a.c b.c
然后gcc获得动态库:
gcc -shared a.o b.o -o libxxxx.so
编译语句和静态库相同,但是动态库并不会被打包到可执行文件中。

  • 程序启动之后,动态库会被动态加载到内存中,可以通过 ldd (list dynamic dependencies)命令检查动态库依赖关系

运行时动态加载需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。
对于 elf 格式的可执行程序,是由 ld-linux.so 来完成的,它先后搜索
elf 文件的 DT_RPATH段
——> 环境变量 LD_LIBRARY_PATH
——> /etc/ld.so.cache 文件列表
——> /lib/,/usr/lib 目录找到库文件后将其载入内存

由于自定义动态库在这些路径都没有,因此会报错
解决方法:

  1. 配置 LD_LIBRARY_PATH :在.bashrc 中export LD_LIBRARY_PATH = ${LD_LIBRARY_PATH}:动态库所处的绝对路径
  2. 配置 /etc/ld.so.cache :需要通过/etc/ld.so.conf配置,然后运行ldconfig,就可以连接到了
  3. 将动态库添加到 /lib/,/usr/lib (不推荐)

Makefile

打包的shell指令

Makefile文件:
目标xxxx :依赖xxxx
	命令
	。。。(必须有缩进) 

Makefile上一条指令如果缺少依赖,会向下运行。如果下方命令和第一条命令无关,不会被执行

会自动检测文件更新,未更新的文件不需要新编译

变量

var=hello
#获取变量的值 $(变量名)
$(var)

预定义变量:

  • AR : 归档维护程序的名称,默认值为 ar
  • CC : C 编译器的名称,默认值为 cc
  • CXX : C++ 编译器的名称,默认值为 g++
  • $@ : 目标的完整名称
  • $< : 第一个依赖文件的名称
  • $^ : 所有的依赖文件

例:

#自动变量只能在规则的命令中使用
app:main.c a.c b.c
	$(CC) -c $^ -o $@

可使用通配符进行模式匹配:

  • %.o:%.c
    %: 通配符,匹配一个字符串
    两个%匹配的是同一个字符串
  • %.o:%.c
    gcc -c $< -o $@

函数

$(wildcard PATTERN…)

  • 功能:获取指定目录下指定类型的文件列表
  • 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用
    空格间隔
  • 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔

$(patsubst ,)

  • 功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以
    替换。
  • 可以包括通配符 % ,表示任意长度的字串。如果中也包含 % ,那么,中的这个 % 将是中的那个%所
    代表的字串。(可以用 \ 来转义,以 % 来表示真实含义的 % 字符)
  • 返回:函数返回被替换过后的字符串

clean命令

make clean
会运行

clean:
    rm -f $(OBJS) app

GDB

文件IO

open(path, mode)
read()
write()
文件带有一个结构体:stat,保存文件的基本信息
ret = lseek(fd, 0, SEEK_END); 可以获取文件长度
请添加图片描述

  • Linux 系统函数:数据从用户空间缓冲区复制到内核页缓存,内核异步将数据刷新到磁盘。
  • 标准 C 库函数:数据先写入标准 C 库的用户空间缓冲区,在缓冲区满或调用 fflush() 时,数据从用户空间缓冲区复制到内核页缓存,再由内核异步刷新到磁盘。

进程

linux命令

  • ps aux:显示所有进程的详细信息,a=all x:显示没有控制终端的进程

  • ps ajx: 显示与作业控制有关的进程
    在这里插入图片描述

  • 实时显示动态:top 在显示结果界面可以输入命令更改排序或杀死某个进程

函数

父进程号(PPID)

getid:

pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid);

fork

子进程返回的pid是0,父进程就是子进程的id,子进程有父进程的所有数据的副本(除了某些共享段)
当两者打开同一个文件,二者输出会相互混合
二者执行先后顺序随机

pid_t fork(void);

GDB调试:使用show|set follow-fork-mode (parent|child)选择调试的程序,运行还是父子进程同时运行,如果调试子进程,父进程将不受断点控制
set detach-on-fork [on | off] 调试时其他进程是否脱离调试继续运行(on),挂起就是off
查看调试的进程 info inferiors
切换 : inferior num

exec族

#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[]);

新程序会继承原进程的内核区数据(进程ID,pwd,控制终端)
替换用户区数据(堆、栈、bss、数据区、代码区)
excl族:单独传参数,然后传入(char * )0
excv族:传入参数数组(vector),数组同样用null结尾
exc*p族:传入一个路径或一个文件名,如果是路径名,直接读取路径,如果是文件名,那么会从环境变量寻找该文件名(可以是一个shell脚本,此时filename作为shell命令输入)
exc**e:带e说明最后可以传入一个指向环境字符串指针数组的指针

exit和_exit

区别在于是否刷新io
在这里插入图片描述

孤儿进程与僵死进程

孤儿:
提前结束的父进程的子进程会被init进程收养,然后等待善后
僵死:
提前结束的子进程资源被父进程管理,但父进程还没结束,不调用wait()或waitpid(),无法释放子进程,发生僵死,进程号被一直占用(进程号是有限的),进程状态表现为Z

wait和waitpid

当子进程结束时,将返回一个SIGHOLD信号。
调用wait会将父进程阻塞,等待子进程结束,发送信号给父进程,wait将立即返回结束进程的编号,传入的 *statloc 将被写入子进程的返回状态
wait调用成功返回进程编号,失败会返回-1或0(根据系统而定,一般是-1)
在这里插入图片描述
waitpid(等待某个特定进程结束再继续)

管道

  • 管道如果读端停止,写端立即停止(防止管道破裂)
  • 写端停止,读端会阻塞,等待写端继续写入

分为有名管道和匿名管道
匿名管道只能在父进程与子进程或子进程之间通信,返回值成功为0,失败为-1

  • int pipe(int fd[2]) fd[0]是读,fd[1]是写
  • read(fd[0], *char[], len)
  • write(fd[1], *char[], len)
  • 可设置两个文件描述符的模式:是否阻塞(fcntl)

有名管道通过创建管道文件读写数据

makefifo(*path, mode)

  • 只打开一个只读或只写管道会阻塞,等待另一个只写或只读管道打开
  • 管道有数据,read返回
  • 管道无数据
    – 写端被关闭,read返回0
    – 写端没关闭,读端阻塞
  • 读端关闭,写端会被终止(SIGPIPE)
  • 管道没关闭:
    – 管道满,写端阻塞
    – 管道不满,写端write写入数据,返回实际写入的字节数

内存映射

将磁盘内数据映射到内存地址空间中,程序即可直接操作内存,操作的数据会同步到磁盘中

有名映射

void* ptr = mmap(addr, len, prot,flags, fd, off_t offset);

参数prot:映射区域的保护方式。可以为以下几种方式的组合:

PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取

参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。

MAP_FIXED
如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED
对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE
对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy onwrite)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS
建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE
只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将
映射区域锁定住,这表示该区域不会被置换(swap)。

  • 打开文件的长度len必须大于零
  • 偏移量必须是4k的整数倍
  • prot必须有读权限才能写PROT_READ|PROT_WRITE
  • .open一个新文件来创建映射区的话,必须通过lseek或truncate对新文件大小进行扩展,大小为0一定不可用
    • truncate(file, size);

释放:munmap(ptr, len)

信号

在这里插入图片描述
在这里插入图片描述

信号捕捉函数

  1. void (*signal(int signo, void (8func)(int))(int handler)

    • signum 要捕捉的信号
    • handler:SIG_IGN 忽略 、SIG_DEL默认行为 、回调函数
  2. int sigaction(int signo, cont struct sigaction *restrict act, oact)

    • 未决信号集:信号产生,但是没有被处理
    • 阻塞信号集:信号被当前进程捕捉,但是被阻塞,没有被处理(处理指系统默认动作或进程捕获该信号),此时被阻塞的信号将保持未决状态,(多个阻塞信号可能产生信号排队),当它解除阻塞,或该信号动作设置为忽略,此时解除未决状态
    • 这样进程在信号递送给他之前仍可以改变对该信号的动作

对于多个相同的信号被解除

阻塞的情况,未决信号集只会记录该信号的一次出现

信号集相关函数

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);

设置阻塞信号集需要先清空原来预定义的信号集

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);

|信号捕捉要注意是否在捕捉之前就已经有信号产生了(因为信号捕捉的定义需要时间)
|因此需要先将要捕捉的信号阻塞,定义完捕捉后再将信号非阻塞

// 捕捉子进程死亡时发送的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);

共享内存

shgmdt

守护进程的创建

在 Linux 系统中,守护进程(Daemon)是一种在后台持续运行的进程,它通常在系统启动时启动,在系统关闭时终止,并且不与任何控制终端关联。下面详细介绍创建守护进程的步骤和示例代码。

创建守护进程的步骤

  1. 创建子进程,终止父进程
    通过 fork() 系统调用创建一个子进程,然后让父进程退出。这样做的目的是让子进程在后台继续运行,并且子进程会被 init 进程(进程 ID 为 1)收养。

  2. 创建新会话
    使用 setsid() 系统调用创建一个新的会话。调用该函数的进程将成为新会话的会话首进程、新进程组的组长进程,并且没有控制终端。

  3. 改变工作目录
    使用 chdir() 系统调用将工作目录更改为根目录 /。这样可以避免守护进程的工作目录被卸载,从而导致文件操作失败。

  4. 重设文件权限掩码
    使用 umask() 系统调用重设文件权限掩码。这样可以确保守护进程创建的文件具有合适的权限。

  5. 关闭文件描述符
    关闭所有从父进程继承的文件描述符,通常包括标准输入(文件描述符为 0)、标准输出(文件描述符为 1)和标准错误输出(文件描述符为 2)。可以将它们重定向到 /dev/null,以避免守护进程产生不必要的输出。

  6. 执行守护进程的核心任务
    在完成上述步骤后,守护进程可以开始执行它的核心任务,例如定期检查系统状态、处理网络请求等。

示例代码

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

void daemonize() {
    pid_t pid;

    // 第一步:创建子进程,终止父进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    // 第二步:创建新会话
    if (setsid() < 0) {
        perror("setsid");
        exit(EXIT_FAILURE);
    }

    // 第三步:改变工作目录
    if (chdir("/") < 0) {
        perror("chdir");
        exit(EXIT_FAILURE);
    }

    // 第四步:重设文件权限掩码
    umask(0);

    // 第五步:关闭文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 可选:将标准输入、标准输出和标准错误输出重定向到 /dev/null
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_WRONLY);
    open("/dev/null", O_WRONLY);
}

int main() {
    // 调用守护进程初始化函数
    daemonize();

    // 第六步:执行守护进程的核心任务
    while (1) {
        // 这里可以添加守护进程的核心任务代码
        sleep(1);
    }

    return 0;
}

代码解释

  1. daemonize() 函数:该函数实现了守护进程的创建步骤,包括创建子进程、创建新会话、改变工作目录、重设文件权限掩码和关闭文件描述符。
  2. main() 函数:调用 daemonize() 函数初始化守护进程,然后进入一个无限循环,模拟守护进程的核心任务。在实际应用中,你可以在循环中添加具体的任务代码。

编译和运行

将上述代码保存为 daemon.c,然后使用以下命令编译和运行:

gcc -o daemon daemon.c
./daemon

运行后,守护进程将在后台持续运行。你可以使用 ps -ef 命令查看守护进程的状态。

注意事项

  • 守护进程的核心任务应该具有错误处理和日志记录功能,以便在出现问题时能够及时发现和解决。
  • 守护进程的资源管理非常重要,需要避免内存泄漏和文件描述符泄漏等问题。

关闭标准输入输出的原因

守护进程的特点决定其对标准输入输出需求不大
守护进程是在后台持续运行的进程,一般在系统启动时自动启动,在系统关闭时终止。它独立于控制终端,不需要用户的交互操作,其主要目的是提供系统服务,如网络服务、定时任务处理等。因此,从功能上来说,它没有像普通交互式程序那样对标准输入输出的强烈依赖。
关闭标准输入输出可以:

  1. 避免与控制终端关联
    守护进程的设计理念是在后台默默运行,不与任何控制终端绑定。如果不关闭标准输入输出,它可能会受到控制终端状态的影响,例如当控制终端关闭或出现异常时,可能会导致守护进程异常退出。关闭标准输入输出可以确保守护进程的独立性和稳定性。
  2. 防止产生不必要的输出
    守护进程的运行通常是自动化的,不需要向用户实时反馈信息。如果不关闭标准输出和标准错误输出,守护进程产生的输出可能会在控制终端上显示,干扰用户操作或者造成信息混乱。而且,过多的输出还可能占用系统资源。
  3. 避免资源浪费
    打开的文件描述符会占用系统资源,如果守护进程不需要使用标准输入输出,关闭它们可以释放这些资源,提高系统的资源利用率。

常用函数:

  1. access(path, mode):检查一个文件是否满足mode的条件,不满足返回-1

  2. memset(*ptr[], 要填充的内容, size);

  3. fgets(

  4. ptr[], size, stdin);

    str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
    n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以str 传递的数组长度。
    stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

  5. int kill(pid_t pid, int sig) 给pid发送信号

  6. int raise(int sig) 给当前进程发送信号,成功发送0

  7. void abort(void) 给当前进程发送SIGABRT,杀死当前进程

  8. unsigned int alarm(unsigned int seconds):

    • 计时器是系统计时,无关进程的当前状态
    • 开始倒计时,倒计时为0发送SIGALARM到当前进程。SIGALARM默认终止当前进程
    • 计时器一个程序只有一个,如果当前已经有一个计时器,新计时器会返回当前计时器的剩余时间,然后将计时器设置为新传入的数值,如果之前没有计时器,返回0
  9. int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue) 循环定时器,非阻塞

struct itimerval

{
                struct timeval it_interval; 计时器要循环的时间间隔
                struct timeval it_value;   计时器延迟时间
 };
struct timeval

{
                long tv_sec;long tv_usec;              毫秒
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值