BUAA-SE-SP学习记录
参考资料:《Linux编程基础》

Linux环境基础

常用Linux命令

文件系统管理命令

命令参数作用
cd
ls
pwd
mkdir
touch

系统及用户管理命令

命令参数作用

用户/组权限管理

权限操作命令

su : 切换到root , root 账户具有最高权限。返回当前用户则使用exit 。
sudo : 在指令前加上sudo ,使得本条指令以最高权限运行。
chmod : 使用 chmod 命令更改文件权限。
chown : 使用chown 命令更改文件所有者。
chgrp : 使用chgrp 命令更改文件的所属组。
useradd , groupadd : 添加用户/用户组 格式为 useradd/groupadd [选项] 用户名
passwd : 给用户设置密码。格式为passwd [选项] 用户名
userdel , groupdel : 删除用户 格式为 userdel/groupdel [选项] 用户名
usermod , groupmod : 用以修改用户和用户组的相关属性

# chmod修改权限的两种方式
chmod +x file
chmod 777 file

管道

可以把多个命令串联起来,使一个命令的输出作为另一个命令的输入
命令1 | 命令2 | 命令3 ....| 命令n 

环境变量

环境变量是用来定义系统运行环境的一些参数

默认环境变量

每个用户不同的家目录~HOME
邮件存放位置~MAIL
系统默认的查找可执行文件的路径~PATH

自定义环境变量

env 查看所有环境变量
export 变量名 将一个本地变量修改为环境变量
export 变量名=值 定义一个环境变量

永久设置环境变量

把上面提到的export语句写到~/.bashrc 文件(mac是 ~/.bash_profile 中)
mac使用zsh的话就是 ~/.zshrc 

PATH

PATH 是一个环境变量,这个环境变量指明了系统默认的查找可执行文件的路径。你可以在 bash shell中使用echo $PATH 打印出你当前的PATH
PATH实际上是几个路径之间用: 拼接起来的。
有了PATH ,当你在命令行输入一个程序名时,bash shell 就会去PATH 所指定的这几个目录中去寻找该程序,如果找不到就会报错。

  • 使用which指令查看指令(即程序)的具体路径

    Shell编程

    Shell基础

    shell是命令解析器,和shell交互的程序叫terminal(终端)
    bash也是一种shell
    bash/zsh/tcsh/fish/...
    bash的配置文件是 -/.bash_profile 
    zsh的配置文件是 -/.zshrc 

    脚本的执行

    操作系统在执行该脚本文件的时候,会用一种解释器(bash、Python 等)来逐行解释执行该文件中的指

    bash test.sh 
    python test.py 
    请始终注意,文件拓展名大多数情况下都是给人看的,操作系统不会自动把.py结尾的文件看做是 Python 文件

    指定脚本运行使用的解释器(shabang)

    #!/bin/bash 
    放在脚本第一行,指定所用的解释器
    执行这种脚本只需文件名即可: ./test.sh 

    命令连接符(短路)

    && 只要有假就停止
    || 只要有真就停止

    变量

    变量名=值
    $var
    ((var+=5))

    位置变量(传入参数)

    在执行 Shell 脚本的时候,可以传入参数,如当前有个脚本叫test ,执行sh test arg1
    arg2 arg3 ,那么在test 中, $0 代表脚本文件名, $1 为第一个参数: arg1 ,以此类推。

    Shell指令

    grep

    Globel Search Regular Expression and Printing out the line
    全面搜索正则表达式并把行打印出来

grep 基本正则表达式
grep -E / egrep 扩展正则表达式
不加引号:过滤字符串
加引号:模式匹配
grep -P 支持数字 \d 匹配

read

标准输入
read var 
read -r var 忽略转义符

date

输出当前时间

mv

重命名,或移动文件
mv abc 123 把abc重命名为123

引号符号

把指令的执行结果作为参数,供另一个指令使用

  • 单引号:所见即所得

    • 不会解析里面的变量,全部当作字符串原样输出
  • 双引号:所见非所得

    • 先把变量解析之后,再输出
  • 反引号(``) :命令替换,通常用于把命令输出结果传给入变量中

    • RESULT=md5sum /home/wzx/Desktop.zip``
    • for file in ls

    指令参数

    $0 指令内容
    $1 第一个参数
    $2 第二个参数

    sed

    字符串替换(重命名)
    -r选项: --regexp-extended,支持扩展正则表达式,否则扩展正则表达式的相关符号都要加转义\
    一般前面用echo+管道作为输入
    使用格式:
    sed -r "s/ 修改前_用括号指定修改哪里 / 修改后 /g" 
    正则表达式第1个()括号里面代表第一段字符串即\1

    echo "aaa bbb" | sed -r 's/(.)/A/'
    Aaa bbb
    echo 202.038.008.090 | sed -r 's/0+([0-9]+)/\1/g'
    22.38.8.90
    
    # 使用sed文件重命名(末尾加owner)
    mv $file `echo $file | sed -r "s/(.*)/\1[$owner]/g"`

    stat

    获取文件信息

    owner=`stat -c %U $file`

    Shell控制语句

    条件语句

    if [];then
      xxx
    elif []; then
      xxx
    else
      xxx
    fi
    
    # 在if中使用正则判断
    if [[ $str =~ ^[0-9]+$ ]];then
    
    # 在if中使用通配符判断
    if [[ "$str" == "hello"* ]];then

    while循环语句

    while []
    do
      xxx
    done

    for循环语句

    for var in 1 2 3 4 5 6
    do
      xxx
    done

    case语句

    case $var in
    expr1)
      xxx
    ;;
    expr2)
      xxx
    ;;
    *)
      xxx
    ;;
    esac

    select语句

    select var in "A" "B" "C"
    do
      xxx
    done

    注意菜单列表用双引号扩起来,用空格分割

Shell条件表达式

test 表达式 或 [ 表达式 

逻辑操作

操作含义
!expr 逻辑非 NOT
expr1 -a expr2 逻辑与 AND
expr1 -o expr2 逻辑或 OR

数值比较

表达式含义
-eq是否等于
-nq是否不等于
-gt是否大于
-ge是否大于等于
-lt是否小于
-le是否小于等于

文件操作

操作含义
-d filename 若file为目录,返回为真
-f filename
-s filename

字符串

操作含义
$str1 = $str2判断相等
$str1 != $str2判断不等
-n $str判断非空
-z $str判断空
$str判断非空

Shell正则表达式

基础正则

符号名称含义
^ 行首定位^po
$ 行尾定位conf$
. 单字符13.
[] 字符集[0-9a-zA-Z]
* 匹配前导字符任意次数s*

扩展正则

+匹配前导字符至少一次s+
?匹配前导字符最多一次s?
`()`取或(aabbcc)
{}匹配前导字符几次{3}
\d数字字符^rc\d
\s空白字符\s+

Shell通配符

* 所有文件
p* 所有p开头的文件
*.txt 所有txt文件
data??? data开头,后跟三个字符的文件
[0-9]* 以三个数字开头的文件

文件I/O操作与文件系统

常用的文件操作函数

int create(const char *filename, mode_t mode);
create 函数的功是创建文件,如果创建成功会返回一个文件描述符,创建失败则返回-1。

int open(const char *pathname, int flags(,mode_t mode));
open 函数的功能是打开创者创建一个文件。如果文件打开成功,open 函数会返回一个文件描述符,以后对该文件
的所有操作就可以通过对这个文件描述符进行操作来实现。open 函数有两个形式,其中 pathname 是要打开的文件
名。

int close(int fd);
关闭文件,成功调用则返回 0,否则返回-1。

int read(int fd, const void *buf,size_t length);
函数 read 实现从文件描述符 fd 所指定的文件中读取 length 个字节到 buf 所指向的缓冲区中,返回值为实际读取的
字节数。

int write(int fd, const void *buf,size_t length);
函数 write 实现将 length 个字节从 buf 指向的缓冲区中写到文件描述符 fd 所指向的文件中,返回值为实际写入的字
节数。

int lseek(int fd, offset_t offset, int whence);
lseek()将文件读写指针相对 whence 移动 offset 个字节。操作成功时,返回文件指针相对于文件头的位置。

int fcntl(int fd, int cmd, ...);
用来修改已经打开文件的属性的函数 。

int stat(const char *path,struct stat *buf);
用于获取文件的属性。参数 path 为文件路径。当函数调用成功之后,可以通过读取 buf 中的信息获取文件属性。

int access(const char *pathname,int mode);
用于测试文件是否拥有某种权限。参数 pathname 为文件名,参数 mode 可取四个值,分别表示测试文件是否具有
读、写、执行权限和文件是否存在。如果满足 mode 中值所代表的条件,则返回 0,否则返回-1。

int chmod(const char *path,mode_t mode);
用于修改文件的访问权限。

int truncate(const char *path,off_t length);
用于修改文件大小。

目录

Linux目录结构

image.png

inode 索引节点

inode包含文件的元信息
image.png

inode的内容

  • 文件的字节数
  • 文件拥有者的User ID
  • 文件的Group ID
  • 文件的读、写、执行权限
  • 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
  • 链接数,即有多少文件名指向这个inode
  • 文件数据block的位置

每一个文件数据都对应唯一的inode
可以有很多文件名指向同一个inode
stat filename 查看
注意不包含文件名!

inode的大小

操作系统自动在硬盘中保留inode区来存放

inode号码

每个inode都有唯一的号码,操作系统只通过inode号码区分不同文件
文件名-inode号码-inode信息-文件数据位置-读出数据

目录文件

目录的储存形式也是文件,是保存着一系列目录项的列表
每个目录项=文件名+inode号码
ls -i 命令查看inode号码
ls -li 带inode号码的文件详细信息

文件与目录的属性

硬链接

多个文件名指向同一个inode号码

可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。

ln 源文件 目标文件 
每多一个指向inode的文件名,这个inode的链接数+1,每少一个就-1

软链接

是一个新文件,分配一个新的inode
A和B的inode不同,A的内容是B的路径,读取A时自动导向B

软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。

ln -s 源文件 目标文件 

文件权限(ls -l)

drwxrwxr-x 2 leozhudd  staff 4096 2007-10-26 17:20 Desktop
-rw-r--r-- 1 leozhudd  staff  233 2007-10-26 13:10 test.sql
  • 第一个字符

    • d:目录
    • -:文件

之后三个一组,共有三组,分别对应拥有者/拥有组/其他人的权限

  • r:读取
  • w:写入
  • x:执行
  • 第二个:文件硬连接数目

    为了简便,可以用数值来描述权限信息, r 对应的数值是4 , w 对应的数值是2 , x 对应的数值是1 。每组
    权限信息是对这三个数值的简单相加,比如上图中的权限信息可以描述成755 (注意,这是一个八进制数)。

配合 chmod 命令修改文件权限

文件描述符 fd

非负整数,对应一个进程中打开的文件
一个文件可以被同一个进程打开多次

文件描述符实际上是一个索引值,其作用是索引到该进程的文件描述符表中的对应表项。文件描述符表的表项里有一指针,指向在内核中的打开文件表的表项,该表项中存有打开文件的文件偏移量、文件相关目录项 dentry 等相关属性信息。

标准文件I/O

文件 I/O

文件 I/O 称之为不带缓存的 IO(unbuffered I/O)。不带缓存指的是每个 read,write 都调用内核中的一个系统调用。也就是一般所说的低级 I/O——操作系统提供的基本 IO 服务,与 os 绑定,特定于 linux 或 unix 平台。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>
#define MAXLEN 64
int main()
{
    // open测试
    int fd = open("/Users/leozhudd/Desktop/课程Doc/System Programming/sp-labs/lab04/src/t3", O_RDWR);
    if(fd < 0){
        perror("lab04/src/t3");
        return 0;
    }

    // 申请一块内存用来存放数据
    char* buff = (char*)malloc(MAXLEN);
    memset(buff,0,MAXLEN);

    // read测试
    read(fd, buff, MAXLEN);
    printf("%s\n",buff);

    // write测试
    strcpy(buff, "May the force be with you, zhumuqing!");
    lseek(fd, 0, SEEK_SET);
    write(fd, buff, strlen(buff));

    // 输出文件当前内容(修改之后的)
    lseek(fd, 0, SEEK_SET);
    read(fd, buff, MAXLEN);
    printf("%s\n",buff);

    // 文件关闭
    close(fd);
    return 0;
}

标准 I/O

标准 I/O 是 ANSI C 建立的一个标准 I/O 模型,是一个标准函数包和 stdio.h 头文件中的定义,具有一定的可移植性。标准 I/O 库处理很多细节。例如缓存分配,以优化长度执行 I/O 等。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAXLEN 64
int main()
{
    // fopen测试
    FILE* fp = fopen("/Users/leozhudd/Desktop/课程Doc/System Programming/sp-labs/lab04/src/t3","r+");
    if(fp == NULL){
        perror("lab04/src/t3");
        return 0;
    }

    // 申请一块内存用来存放数据
    char* buff = (char*)malloc(MAXLEN);
    memset(buff,0,MAXLEN);

    // fread测试
    fread(buff, sizeof(char), MAXLEN, fp);
    printf("%s\n",buff);

    // fwrite测试
    strcpy(buff, "May the force be with you, zhumuqing!");
    fseek(fp, 0, SEEK_SET);
    fwrite(buff, sizeof(char), strlen(buff), fp);

    // 输出文件当前内容(修改之后的)
    fseek(fp, 0, SEEK_SET);
    fread(buff, sizeof(char), MAXLEN, fp);
    printf("%s\n",buff);

    // 文件关闭
    fclose(fp);
    return 0;
}

对比

文件 I/O 中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准 I/O 中用 FILE(流)表示一个打开的文件,通常只用来访问普通文件。
标准I/O是ANSI C标准中的C语言库函数,在不同的操作系统中应该调用不同的内核API,UNIX环境下,标准I/O是对文件I/O的封装。

文件I/O标准I/O
打开文件openfopen, freopen
关闭文件closefclose
读取readgetc, fgetc, getchar

fgets, gets,
fread |
| 写入 | write | putc, fputc, putchar
fputs, puts,
fwrite |

有关标准I/O拥有的缓冲区:
open/read/write和fopen/fread/fwrite的区别
open/read/write和fopen/fread/fwrite的区别

open和fopen和freopen

open:打开文件,返回文件表示符
fopen:打开文件,返回文件指针
freopen:把标准输入/输出流stdin/stdout重定向到指定文件

int fd = open(char* path, int flags(, mode_t mode));
// path 文件所在路径
// flag = O_RDONLY/O_WRONLY/O_WDLR/O_CREAT
// 其中O_CREAT是如果文件不存在就新建文件,对应需要mode参数指定权限(0744)

FILE* fp = fopen(const char* path, const char* mode);
// mod = r/r+/w/w+/a/a+/...

FILE *freopen( const char *path, const char *mode, FILE *stream ); 
// 一般可以不使用它的返回值

read/write和fread/fwrite

ssize_t numread = read(int fd, void *buf, size_t length);
ssize_t result = write(int fd, void *buf ,size_t length);
// buf指向要读写的数据内存地址,length是要写入的字节数

fread(void* buf, size_t size, size_t count, FILE* fp);
fwrite(void* buf, size_t size, size_t count, FILE* fp);
// size是要读写的单个元素的字节数,count是进行多少个size字节的元素的读写
// 返回值:读取的总数据元素个数

close和fclose

int close(int fd);
int fclose(FILE* fp);

lseek和fseek

移动文件读写指针

// 移动到文件开头
lseek(fd, 0, SEEK_SET);
fseek(FILE* fp, 0, SEEK_SET)

fflush

定义函数:int fflush(FILE* stream);
函数说明:fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中. 如果参数stream 为NULL,fflush()会将所有打开的文件数据更新.

文件I/O的dup函数(复制文件描述符)

使多个文件描述符指向同一个文件

#include <unistd.h> 
int dup(int oldfd); 
int dup2(int oldfd, int newfd);

当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。
dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。

C语言的Linux目录操作

基于 dirent.h 中的 opendir 和 readdir 
具体使用方法看下面代码

// 要求输入一个参数代表指定路径,打印路径下所有文件的名称
// SP-Lab04-6
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>

void list_files(DIR* dir){
    struct dirent* pdir;// dirent结构保存每个文件的信息,即pdir是指向文件的指针

    // 通过句柄dir,调用readdir来获取目录下的文件
    while((pdir = readdir(dir)) != NULL){
        // 不打印.和..这两个目录
        if(strcmp(pdir->d_name,".") == 0  || strcmp(pdir->d_name,"..") == 0) continue;
        printf("%s\n", pdir->d_name);
    }
}

int main(int argc,char *argv[])
{
    DIR* dir;

    if(argc < 2){
        printf("参数数量不正确!");
        return 1;
    }
    
    // 打开目录,获取目录句柄
    if((dir = opendir(argv[1])) == NULL){
        perror(argv[1]);
        return 1;
    }

    list_files(dir);

    return 0;
}

文件锁

详情有待补充(教材)
请看代码注释

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    int fd;
    struct flock lock,savelock;// 两个文件锁对象

    fd = open("book.dat", O_RDWR);
    lock.l_type = F_WRLCK;// 定义一个写入锁
    lock.l_start = 0;
    lock.l_whence = SEEK_SET;
    lock.l_len = 0;
    savelock = lock;// 保存在savelock

    fcntl(fd, F_GETLK, &lock);// 获得当前的文件锁信息,保存在lock
    if(lock.l_type == F_WRLCK){
        printf("Process %d has a write lock already!\n", lock.l_pid);
        exit(1);
    }
    else if(lock.l_type == F_RDLCK){
        printf("Process %d has a read lock already!\n", lock.l_pid);
        exit(1);
    }
    else
        fcntl(fd, F_SETLK, &savelock);// 未被锁,则设置写入锁

}

Linux进程管理

进程的概念

一个进程包括以下内容:程序代码(文本),当前活动(程序计数器,寄存
器的值),堆栈,数据端,堆
image.png

进程的创建

每个进程都有一个非负整型表示的唯一进程 ID—— pid
在命令后面加 & 符号:进程后台运行

创建子进程

创建一个子进程,共享父进程所有内容,并且这个子进程会接着 fork 下面的代码继续执
行。

#include <unistd.h>
pid_t result = fork();

fork()返回值:

  • 出错返回-1
  • 子进程返回0
  • 父进程返回子进程ID

fork()的两种用法:

  • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段
  • 一个进程要执行一个不同的程序。在这种情况下,子进程从fork返回后立即调用exec

    // 同时创建多个子进程的方法
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
      pid_t pid;
      for(int i=1;i<=5;i++){
          pid = fork();
          //循环中,fork函数调用五次,子进程返回0,父进程返回子进程的pid,
          //为了避免子进程也fork,需要判断并break
          if(pid == 0) break;
      }
      if(pid > 0){
          printf("父进程: pid= %d\n", getpid());
          sleep(1); //这里延迟父进程程序,等子进程先执行完。
      }
      else if(pid == 0){
          printf("子进程: pid= %d\n", getpid());
      }
      return 0;
    }

    使用exec函数执行新的程序

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

当进程调用一种exec 函数时,该进程执行的程序完全替换为新程序,而新程序从其main函数开始执行。
调用exec 并不创建新进程,前后的进程 ID 并未改变,exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);

int execv(const char *pathname, char *const argv[]);

int execle(const char *pathname, const char *arg0, ...
/* (char *)0, char *const envp[] */);

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);

int execvp(const char *filename, char *const argv[]);

int fexecve(int fd, char *const argv[], char *const envp[]); // 第一个参数使用
的是打开的文件描述符,而非文件路径名

// 7个函数返回值:若出错,返回-1;若成功,不返回
// execl函数实例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    pid_t pid = fork();
    if(pid == 0) {
        // 子进程转移执行另一个程序
        execl("calc","calc","0","3","4");// 传给那个程序的命令行参数
    }
    waitpid(pid,NULL,0);
    puts("程序结束");

    return 0;
}

进程退出

守护进程

脱离于终端控制,并且在后台运行的进程

僵尸进程

当子进程先于父进程结束,同时父进程没有使用 wait() 获取子进程的结束状态时,子进程就成为僵尸进程

孤儿进程

在子进程终止之前,父进程先终止

使用exit处理进程终止

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

等待进程终止并获取退出状态

在父进程调用 wait() 等待子进程结束,并获取进程的返回状态(结束时传给exit的值)
wait()会暂时停止目前进程的执行, 即阻塞父进程,等待子进程结束或者其他信号。

#include <sys/wait.h>
pid_t wait(NULL);
pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *statloc, int options); // 可以指定等待的子进程pid

返回值

  • 成功:返回子进程pid
  • 失败:返回-1

    进程间通信

    管道、信号量、共享内存
    可以通过ipcs指令查看以上内容
    https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/ipcs.html#

    重定向

    重定向产生的原因就是文件描述符在分配时趋向于数值小的,而在用户层,stdout 这个文件指针指向的文件已经封装了,并且它的 fd 就是 1,这是不能修改的,所以我们一上来关闭了 1 号文件,然后新创建了一个文件它的文件描述符就会分配为被 1,同时此时写入时,像 printf 这类函数默认使用的输出流就是 stdout,但是我们知道它的 1 指向的已经是我们新生成的那个文件了,所以这就重定向的本质。

    // 两种方法
    // 第一种:使用文件IO
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/file.h>
    int main(int argc, char *argv[])
    {
      close(1);
      open("log.txt",O_WRONLY);
      printf("233");
      return 0;
    }
    
    // 第二种 使用标准IO
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
      fclose(stdout);
      fopen("log.txt","w");
      printf("233");
      return 0;
    }

    管道

    管道是最基本的进程通信机制,可以想象成一个管道,两端分别连着 2 个进程,一个进程往里面写,一
    个进程从里面读。如果读或写管道的时候没有内容可供读或写,进程将被阻塞,直到有内容可供读写为
    止。

匿名管道

匿名管道创建后本质上是 2 个文件描述符,父子进程分别持有就能够使用管道,需要注意的是不能够共用匿名管道,也就是除了使用的进程,其他进程需要关闭文件描述符,保证管道的 2 个描述符分别同时只有 1 个进程持有。

#include <unistd.h>
int pipe(int filedes[2]);
// 调用成功返回0,失败返回-1

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
image.png

// 父子进程间管道通信实例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>

char sendbuf[] = "A_song_for_You!_You?_You!!";
char recbuf[20];
char parrecbuff[20];

int main(int argc, char *argv[])
{
    // 创建管道所用的文件描述符数组
    int filedes[2];
    pid_t pid;
    if(pipe(filedes) < 0) {
        perror("pipe failed");exit(1);
    }
    // 创建子进程
    if((pid=fork()) < 0) {
        perror("fork failed");exit(1);
    }
    if(pid == 0) {
        read(filedes[0], recbuf, strlen(sendbuf)); //2. 从管道接收数据
        write(filedes[1], recbuf+strlen(recbuf)-5, 5); //3. 把最后五位重新发送回管道
    }
    else {
        printf("The begining str is: %s\n",sendbuf);
        write(filedes[1], sendbuf, strlen(sendbuf)); //1. 向管道发送数据
        sleep(5); //4. 等待子进程从管道将数据取走并再次返回
        read(filedes[0], parrecbuff, strlen(sendbuf));
        printf("The final str is: %s\n",parrecbuff);
        wait(NULL);
    }
}

有名管道

无名管道只能在父子进程间通信,而有名管道没有这个限制。
命名管道是一种特殊类型的文件(可以用 ls -l filename 查看)
命名管道是根据路径来使用管道, 故能够在任意进程间通信。

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

可以用open系统调用打开管道文件,注意打开时权限不要选O_RDWR,因为FIFO管道是单向的

// 通过有名管道发送文件内容
// A.c
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
int main()
{
    int fd;
    char s[150];
    // 先把文件内容读入字符串保存
    FILE* fp = fopen("./file", "r");
    fgets(s, sizeof(s), fp);
    fclose(fp);
    // 创建有名管道并写入数据
    mkfifo("./named_FIFO", 0777);
    fd = open("./named_FIFO", O_WRONLY);
    write(fd, s, strlen(s) + 1);
    close(fd);
    return 0;
}

// B.c
int main()
{
    int fd;
    char s[150];
    // 打开管道并读入数据到字符串
    fd = open("./named_FIFO",O_RDONLY);
    read(fd, s, 1024);
    close(fd);
    // 把字符串写入文件
    FILE* fp = fopen("./file2", "w+");
    fputs(s, fp);
    fclose(fp);
    return 0;
}

消息队列

消息队列本质上在内核空间中开辟了一块内存空间,这块内存是其他进程可以访问到的,在其中使用链
表的方式实现了一个队列,进程可以向该队列中发送数据块或读取数据块,从而达到进程间通信的目
的。其中每个数据块包含两部分,首先是一个类型为 long 的 type,然后是具体的数据,其中的这个
type 就可以作为进程之间相互约定好的协议,即你发送 type 为15131049 的消息,我接收 type 为
15131049 的消息,我确认这就是你发出来的,我信任该数据块中的数据。
image.png
消息类型(type):大于0的整数,每条消息都有自己的消息类型。其中类型为0的消息维护所有消息加入队列的顺序(红色)

// 创建一个消息队列,key是消息队列编号
// flag指定权限:IPC_CREAT表示如不存在就创建新的消息队列,加上IPC_EXCL表示创建新的消息队列,无法创建时报错,通常要加上操作权限,比如0666
// 返回值是新建的消息队列id
int msgget(key_t key,int flag);

// 定义消息结构
typedef struct {
    long type;  // 消息类型(正整数)
    char message[255];  // 消息正文
}Msg;

// 将消息发送到消息队列
// msqid=ipc内核对象id,msgp=消息数据地址,msgsz=消息正文大小
// msgflg =0自动阻塞/=IPC——NOWAIT不阻塞
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

//删除之前的消息队列   
msgctl(id_up, IPC_RMID, 0);

信号量

信号量最好理解为“信号灯”,就相当于红绿灯 🚦一样,用来在进程遭遇“岔路口”的时候,通知进程做怎样的工作。其本质是为了实现多个进程之间的同步。
信号量是一种数据结构,说明某个资源可用的数量,用于多进程对共享数据对象的读取。

  • 进程使用资源执行 P(sv) 操作,信号量-1
  • 如果-1后大于等于0,则正常执行,如果小于0,则把当前进程放进等待队列
  • 进程执行完后释放资源执行 V(sv) ,从等待队列中选一个进程执行,如果队列为空则sv+1

    二值信号量

    如果资源只有一份,那么信号量的初始值将是1 ,最多只能有一个进程使用该资源。这种信号量被称
    为“二值信号量”。这时信号量的作用就失去了指示“当前还有多少资源可用”的意义,仅仅用来标明“当前
    资源是否可用”,就蜕化成了“互斥锁”的作用(当然,二值信号量与互斥锁有本质的不同),其作用就类
    似于大家在 Java 中学到的synchronized (当然, synchronized 要实现的是线程之间的同步)。

    SystemV无名信号量

    只能用于父子进程或同一进程多个线程之间。

    // 创建信号量
    int semget(key_t key,int nsems,int flags)
    /*
    (1)第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。
    通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。
    (2)第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。
    (3)第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。
    返回:这个信号量的标识符id
    */
      
    // 信号量操作控制buf
    struct sembuf sembuf;
    sembuf.sem_num = 0; //除非使用一组信号量,否则它为0 
    sembuf.sem_op = +1; // 信号量在一次操作中需要改变的数据,-1:P(等待)操作, +1:V(发送信号)操作
    sembuf.sem_flg = SEM_UNDO; // 通常为SEM_UNDO,使操作系统跟踪信号量,在进程没有释放该信号量而终止时,操作系统释放信号量 
    
    // 改变信号量的值
    int semop(int semid, struct sembuf *sops, size_t nops);
    // nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
    
    // 结合上面两点,封装P/V操作
    int sem_p(int sem_id){
      struct sembuf sembuf;
      sembuf.sem_num = 0;
      sembuf.sem_op = -1;
      sembuf.sem_flg = SEM_UNDO;
      if(semop(sem_id,&sem_buf,1)==-1){
          perror("P opration failed");
          return -1;
      }
      return 0;
    }
    int sem_v(int sem_id){
      struct sembuf sembuf;
      sembuf.sem_num = 0;
      sembuf.sem_op = 1;
      sembuf.sem_flg = SEM_UNDO;
      if(semop(sem_id,&sem_buf,1)==-1){
          perror("V opration failed");
          return -1;
      }
      return 0;
    }

    POSIX有名信号量

    信号值保存在文件中,可实现任意两个进程的通信。

    // 1. 创建
    sem_t* sem_open(const char* name, int oflag, mode_t mode, int value);
    // name=文件路径,oflag=创建模式,mode_t=访问权限,value=信号量初始化值
    // 例如:
    sem_t *mysem;
    mysem = sem_open("POSIXSEM", O_CREAT, 0666, 1);
    
    // 2. 操作
    sem_wait(); // P操作
    sem_post(); // V操作
    // 例如:
    sem_wait(mysem);
    
    // 3. 结束
    sem_close(mysem);
    sem_unlink("POSIXSEM");

    共享内存

    image.png

    // 子进程写入共享内存,父进程读出来
    #include<sys/shm.h>
    #include<sys/ipc.h>
    #include<stdio.h>
    #include<sys/types.h>
    #include<unistd.h>
    #include<string.h>
    int main()
    {
      int shmid; // 共享内存段标识符
      char *shmaddr;  // 共享内存映射地址(可读写)
      char buff[BUFSIZ];
      int shmstatus; // 获取共享内存属性信息
      shmid = shmget(IPC_PRIVATE, BUFSIZ, IPC_CREAT | 0600); // 创建共享内存
      shmaddr = (char *)shmat(shmid, NULL, 0); // 映射共享内存地址
    
      pid_t pid;
      if((pid=fork()) == 0){ // 子进程
          strcpy(shmaddr, "HELLO SHM!");
          shmdt(shmaddr); // 释放所指向的地址
          return 0;
      }
      else if(pid > 0){ // 父进程
          sleep(5);
          strcpy(buff, shmaddr);
          printf("Got from shared memory: %s\n",buff);
          shmdt(shmaddr); // 释放所指向的地址
          shmctl(shmid, IPC_RMID, NULL); // 退出时删除共享内存实例
      }
      return 0;
    }

    信号及信号处理

    信号基本概念

    Linux进程间异步通信的机制
    信号传递一种信息,接收方根据信息进行相应动作
    include <signal.h> 

    信号的产生

  • Ctrl+C/Ctrl+/Ctrl+Z
  • 非法内存
  • 硬件异常
  • 环境切换
  • 系统调用kill/raise/sigsend

    信号的状态

  • 递送(delivery):进程对信号采取动作
  • 未决(Pending):信号从产生到递达之间的状态
  • 阻塞(block):进程可以选择阻塞某个信号,此时信号就会处于Pending状态,直到阻塞解除或忽略处理

    信号的分类

    可靠/不可靠信号

    (早期机制)信号值小于SIGRTMIN为不可靠信号,同时发生多个信号时,只保留一个,剩下被丢掉
    信号值在SIGRTMIN和SIGRTMAX之间为可靠信号,同时发生多个信号时,排入队列依次处理

    实时/非实时信号

    使用 kill -l 命令查看
    前32种为非实时信号(不可靠,可能丢失)
    后32种为实时信号(可靠,支持排队)

    信号的发送和处理

  • 默认处理 signal(SIGINT,SIG_DEF)
  • 忽略信号 signal(SIGINT,SIG_IGN) 
  • 捕捉并处理 signal(SIGINT,func) 

    信号的捕捉

  1. signal函数

    signal(int signum, void (*action)(int)) 
    // signum为要捕捉的信号
    // action为自定义信号处理函数,也可以为SIG_DEF/SIG_IGN,即DEFAULT/IGNORE
  2. sigaction函数

    int sigaction(int signum, const struct sigaction* act, const struct sigaction* oldact);
    // signum:要捕捉的信号
    // act:sigaction类型的结构体,包含自定义处理函数和其他信息
    // oldact:传出参数,包含旧处理函数等信息(一般为NULL)
    
    // 系统写好的结构体,可以直接创建
    stuct sigaction {
       void (*)(int) sa_handle;
       sigset_t sa_mask;
       int sa_flags;
    }
    
    // 实例1(信号有附加信息)
    struct sigaction act;
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = sigHandler;
    sigaction(SIGUSR1, &act, NULL);
    
    // 实例2(简单信号)
    struct sigaction act;
    act.sa_handle = sigHandler;
    sigaction(SIGUSR1, &act, NULL);
  3. 捕捉后的信号处理函数

    // 如果信号没有附加数据
    void handler(int sig){};
    
    // 如果要接收信号附加数据
    void handler(int sig, siginfo_t *info, void *ucontext){};
    //第一个参数sig代表接收信号的值
    //第二个参数info是指向siginfo_t类型的指针,包含了有关信号的附加信息
    //第三个参数ucontext是内核保存在用户空间的信号上下文,一般不使用该参数
    
    此时如果接收进程使用sigaction()注册信号处理函数,并将sa_flags字段置为SA_SIGINFO,那么在
    信号处理函数中可以通过info参数的si_value 获取到发送信号伴随的数据,
    如info->si_value.sival_int 或info->si_value.sival_ptr 。

    信号的发送

  4. kill函数

    int kill(pid_t pid, int sig);
    // pid代表接收信号的进程PID,sig表示要发送什么信号
  5. sigqueue函数

    int sigqueue(pid_t pid, int sig, const union sigval value);
    // pid代表接收信号的进程PID,sig代表要发送的信号,value是联合体代表要传递的数据
    
    // sigval的原型,用于给信号附加数据
    union sigval {
     int sival_int;
     void *sival_ptr;
    };
    
    // 实例
    union sigval mysigval;
    mysigval.sival_int = 114514;
    sigqueue(pid, SIGUSR1, mysigval);

信号的屏蔽

在sigaction中屏蔽

sa_mask:信号屏蔽集,可以通过函数sigemptyset/sigaddset等来清空和增加需要屏蔽的信号。

// 对信号SIGINT处理时,如果来信号SIGQUIT,其将被屏蔽
// 但如果在处理SIGQUIT,来了SIGINT,则首先处理SIGINT,然后接着处理SIGQUIT
struct sigaction act;
act.sa_handler = sigHandler;
// 下面设置要屏蔽的信号
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);

全局信号集屏蔽:sigprocmask

信号集:多个信号编成一个集合,对集合内的信号进行同样的操作
信号集在使用前一定要用sigemptyset或sigfillset初始化

// 信号集设定函数
int sigemptyset(sigset_t *set); //将set指向的信号集初始化为不包含任何信号
int sigfillset(sigset_t *set);  //将set指向的信号集初始化为包含所有信号
int sigaddset(sigset_t *set, int signo);//向信号集添加信号signo
int sigdelset(sigset_t *set, int signo);//向信号集删除信号signo
int sigismember(const sigset_t *set, int signo);//判断信号signo是否在信号集中

sigprocmask:用来修改进程的信号屏蔽字,它可以屏蔽某个信号会对某个已经屏蔽的信号解除屏蔽

// 信号集函数
int sigprocmask(int how,const sigset_t* set,sigset_t* oldset);
//第一个参数用于设置位操作方式,第二个参数一般为用户指定信号集,第三个参数用于保存原信号集
//how=SIG_BLOCK:mask=mask|set
//how=SIG_UNBLOCK:mask=mask&~set
//how=SIG_SETMASK:mask=set

// 实例
sigset_t sigset;
sigfillset(&sigset); // 设置信号集为全部信号
sigprocmask(SIG_SETMASK, &sigset, NULL); // 设置信号集屏蔽

定时信号

alarm函数

#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//第一个参数seconds用来指明时间,经过seconds秒后发送SIGALRM信号给当前进程,当参数为0则取消之
前的闹钟

返回值:
如果本次调用前已有正在运行的闹钟,alarm()函数返回前一个闹钟的剩余秒数
如果本次调用前无正在运行的闹钟,alarm()函数返回0

多线程编程

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

pthread_mutex_t t; // 线程锁

void* T1_exec(void* arg){
    pthread_mutex_lock(&t); // 上锁
    pthread_mutex_unlock(&t); // 解锁
    return NULL;
}
int main()
{
    pthread_mutex_init(&t,NULL); // 线程锁初始化
    
    pthread_t T1; // 线程id
    int ret = pthread_create(&T1, NULL, T1_exec, NULL); // 线程创建并执行
    pthread_join(T1,NULL); // 阻塞,等待子线程结束
    
    pthread_mutex_destroy(&t); // 销毁线程锁
    return 0;
}   
pthread_cond_t cond; // 条件变量
pthread_cond_signal(&cond); // 改变条件(相当于设置flag),并发送广播给其他线程
pthread_cond_wait(&cond, &t); // 等待并释放锁

错误处理

还没看。

Last modification:June 18, 2021
If you think my article is useful to you, please feel free to appreciate