0%

背景

项目文件中有如下类型文件:

Makefile, .sh, .bat, .cfg, .exe

源码用git管理,客户端用cygwin实现windows内的linux环境

问题:如何解决git多人协作下的linux、windows换行符差异问题?

(1)什么是换行符
LF:”\n”,Linux的换行符, 只包含“换行”;
CRLF:”\r\n”,Windows的换行符,包含“回车+换行”;

(2)不同换行符带来什么问题
用git管理代码,必定有远程端和本地端两个仓库,两端的操作系统不同,换行符可能有差异;

多人协作时,本地端可能有linux环境和windows, 如果所有人都是linux就不存在换行符差异的问题;如果有windows和linux就有该问题;
例如A上传了Linux换行符LF的代码到远程,B 本地环境是windows, B git pull下来,其git-config设置了自动转换成本地换行,将代码换行全成了CRLF,B上传后,远程仓库变成CRLF换行。此时A git diff查看,所有代码都有换行差异,扰乱真正的代码diff。

不仅是影响git diff, 换行差异还影响脚本执行

  • 例如LF换行的.sh,git pull到windows环境后换行转换成CRLF, 导致sh无法正常执行;
  • .bat调用.exe读取.cfg内的一行,.exe是windows程序,其readline方法只能识别CRLF换行,无法读取LF换行的.cfg文件内容

git的自动换行符转换配置

参考:core.autocrlf

假如你正在 Windows 上写程序,而其他人用的是其他系统(或相反),你可能会遇到 CRLF 问题。 这是因为 Windows 使用回车(CR)和换行(LF)两个字符来结束一行,而 macOS 和 Linux 只使用换行(LF)一个字符。 虽然这是小问题,但它会极大地扰乱跨平台协作。许多 Windows 上的编辑器会悄悄把行尾的换行字符转换成回车和换行, 或在用户按下 Enter 键时,插入回车和换行两个字符。

Git 可以在你提交时自动地把回车和换行转换成换行,而在检出代码时把换行转换成回车和换行。 你可以用 core.autocrlf 来打开此项功能。 如果是在 Windows 系统上,把它设置成 true,这样在检出代码时,换行会被转换成回车和换行:

$ git config --global core.autocrlf true

如果使用以换行作为行结束符的 Linux 或 macOS,你不需要 Git 在检出文件时进行自动的转换; 然而当一个以回车加换行作为行结束符的文件不小心被引入时,你肯定想让 Git 修正。 你可以把 core.autocrlf 设置成 input 来告诉 Git 在提交时把回车和换行转换成换行,检出时不转换:

$ git config --global core.autocrlf input

这样在 Windows 上的检出文件中会保留回车和换行,而在 macOS 和 Linux 上,以及版本库中会保留换行。

如果你是 Windows 程序员,且正在开发仅运行在 Windows 上的项目,可以设置 false 取消此功能,把回车保留在版本库中:

$ git config --global core.autocrlf false

使用git config --global core.autocrlf input就可以做到windows的CRLF换行自动转换成LF换行存储在git远程仓库,且git pull/clone到本地时维持LF换行,不影响.sh等linux shell script执行。

手动换行符转换

  • dos2unix FilePath

  • unix2dos FilePath

  • windows2linux

    sed -i ‘s/.$//‘ FilePath

  • linux2windows

    sed -i ‘s/$/\r/‘ FilePath

背景

存储设备产品(如SSD/eMMC)的读写速度和稳定性测试,是量产前必不可少的步骤。除了常规的两个设备来回拷贝读写,存储设备做系统盘的情况下,反复的启动,睡眠,休眠,也是重要的测试项。本文介绍这几种测试的原理,工具实现,和调试过程

系统电源状态

ACPI(高级配置与电源接口)是电源配置和接口的规范,供操作系统和应用程序管理所有电源,其定义了几种状态(State):S1~S6
image-20221205155311145

操作系统在ACPI基础上实现各自的电源状态划分
Linux电源状态划分为如下:
image-20221205155406635

其中,常见的几种状态有别称,如S3通常也称Suspend,S4也称Hibernation, S5即shutdown。下面描述这几种状态的区别

S3:
1、将系统当前的运行状态等数据保存在内存中,此时仍需要向RAM供电,以保证后续快速恢复至工作状态
2、冻结用户态的进程和内核态的任务(进入内核态的进程或内核自己的task)
3、关闭外围设备,如显示屏、鼠标等,中断唤醒外设不会关闭,如电源键
4、CPU停止工作

S4:
挂起到硬盘,俗称休眠(Hibernation)将系统当前的运行状态等数据保存到硬盘上,并自动关机。下次开机时便从硬盘上读取之前保存的数据,恢复到休眠关机之前的状态。
譬如在休眠关机时,桌面打开了一个应用,那么下一次开机启动时,该应用也处于打开状态。而正常的关机-开机流程,该应用是不会打开的

S5:
关机并断电,实际上,S5在不同场景下,可以指代关机和重启两种操作,关机是彻底power off的,而重启不是彻底断电,只是重新启动了系统,(当然基本所有设备都被reset),重启!=关机再启动。

测试工具设计

需求:设计工具使系统从S0(working)进入S3/S4/S5状态,维持一段时间,然后退出该状态,在S0维持一段时间,再次进入S3/S4/S5状态,如此反复循环。用户输入工作模式(S3/S4/S5)和循环次数,工具输出运行日志,包含已运行次数和时间戳。

Linux测试脚本实现

Linux环境有现成的工具:rtcwake,参看其man page:

NAME
       rtcwake - enter a system sleep state until specified wakeup time
SYNOPSIS
       rtcwake [options] [-d device] [-m standby_mode] {-s seconds|-t time_t}
DESCRIPTION
       This program is used to enter a system sleep state and to automatically wake from it at a specified time.
       This uses cross-platform Linux interfaces to enter a system sleep state, and leave it no later than a specified time.  It uses any RTC framework driver that supports standard driver model wakeup flags.
       This is normally used like the old apmsleep utility, to wake from a suspend state like ACPI S1 (standby) or S3 (suspend-to-RAM).  Most platforms can implement those without analogues of BIOS, APM, or ACPI.
       On some systems, this can also be used like nvram-wakeup, waking from states like ACPI S4 (suspend to disk).  Not all systems have persistent media that are appropriate for such suspend modes.
       Note that alarm functionality depends on hardware; not every RTC is able to setup an alarm up to 24 hours in the future.
       The suspend setup may be interrupted by active hardware; for example wireless USB input devices that continue to send events for some fraction of a second after the return key is pressed.  rtcwake tries to avoid this problem and it waits to terminal to settle down before entering a system sleep.

其重要option如下:

      -m, --mode mode
              Go into the given standby state.  Valid values for mode are:

              standby
                     ACPI state S1.  This state offers minimal, though real,
                     power savings, while providing a very low-latency
                     transition back to a working system.  This is the
                     default mode.

              freeze The processes are frozen, all the devices are suspended
                     and all the processors idled.  This state is a general
                     state that does not need any platform-specific support,
                     but it saves less power than Suspend-to-RAM, because
                     the system is still in a running state.  (Available
                     since Linux 3.9.)

              mem    ACPI state S3 (Suspend-to-RAM).  This state offers
                     significant power savings as everything in the system
                     is put into a low-power state, except for memory, which
                     is placed in self-refresh mode to retain its contents.

              disk   ACPI state S4 (Suspend-to-disk).  This state offers the
                     greatest power savings, and can be used even in the
                     absence of low-level platform support for power
                     management.  This state operates similarly to Suspend-
                     to-RAM, but includes a final step of writing memory
                     contents to disk.

              off    ACPI state S5 (Poweroff).  This is done by calling
                     '/sbin/shutdown'.  Not officially supported by ACPI,
                     but it usually works.

              no     Don't suspend, only set the RTC wakeup time.

              on     Don't suspend, but read the RTC device until an alarm
                     time appears.  This mode is useful for debugging.

              disable
                     Disable a previously set alarm.

              show   Print alarm information in format: "alarm: off|on
                     <time>".  The time is in ctime() output format, e.g.,
                     "alarm: on  Tue Nov 16 04:48:45 2010".
                     
       -s, --seconds seconds
              Set the wakeup time to seconds in the future from now.

       -t, --time time_t
              Set the wakeup time to the absolute time time_t.  time_t is
              the time in seconds since 1970-01-01, 00:00 UTC.  Use the
              date(1) tool to convert between human-readable time and
              time_t.

只需要写shell script调用rtcwake即可
注意要求跨状态记录日志,S3/S4很容易,运行信息只是被挪到内存、硬盘,不会丢失,但S5会shutdown, 因此循环次数和时间只能记录在掉电不丢失的硬盘里。
S3S4可以用一个脚本完成,而S5需要单独设计
S3S4.sh如下:
接受用户输入:
opt: S3或S4模式;COUNT:循环次数
每次执行rtcwake,日志写入LOG

opt=$1
COUNT=$2
interval=30
s3timer=120
s4timer=120

mkdir -p log
DATE=$(date +%Y-%m-%d)
LOG=log/${opt}_${DATE}.log
cat /dev/null > ${LOG}

echo "=============================== $opt test start ===============================" |tee -a ${LOG}

for (( i=1; i<=$COUNT; i++ ))
do 
    if [ $opt == "s3" ];then
        echo "************************* S3 Cycle: $i start *************************" |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S` "Going to S3, Duration "$s3timer" sec" |tee -a ${LOG}
        sudo rtcwake -m mem -s $s3timer >> ${LOG} 2>&1
        echo `date +%Y-%m-%d' '%H:%M:%S`" Waitable timer triggered." |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S`" Wake up from S3, Cycle "$i"" |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S`" Successfully left sleep state S3..." |tee -a ${LOG}
    elif [ $opt == "s4" ];then
        echo "************************* S4 Cycle: $i start *************************" |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S` "Going to S4, Duration "$s4timer" sec" |tee -a ${LOG}
        sudo rtcwake -m disk -s $s4timer >> ${LOG} 2>&1
        echo `date +%Y-%m-%d' '%H:%M:%S`" Waitable timer triggered." |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S`" Wake up from S4, Cycle "$i"" |tee -a ${LOG}
        echo `date +%Y-%m-%d' '%H:%M:%S`" Successfully left sleep state S4..." |tee -a ${LOG}
    else
        echo "error input, use s3 or s4 as input"
    fi
    echo `date +%Y-%m-%d' '%H:%M:%S` "wake up for $interval seconds" |tee -a ${LOG}
    echo "************************* $opt Cycle: $i finish *************************" |tee -a ${LOG}
    #keep wake up time
    sleep $interval
done

echo "=============================== $opt test finished =============================== " |tee -a ${LOG}

S5必须要解决两个问题:
1.每次测试的信息如何跨越重启
2.如何使系统自动不断的重启

对于1,可以将日志写入固定文件,每次重启后读该文件并追加,知道S5循环结束
对于2,Linux有开机自动启动某些桌面程序、shellscript的机制

S5测试脚本分为三部分:
配置自启动并执行首次重启的脚本:s5_start.sh
执行单次S5的脚本,即自启动调用的脚本:s5.sh
停止S5,清楚自启动配置的脚本:s5_stop.sh

s5_start.sh

#!/bin/bash
USER=$(users)
opt=s5
interval=30
s5timer=180

#config autostart
mkdir -p /home/$USER/.config/autostart
touch /home/$USER/.config/autostart/s5.desktop
echo "
[Desktop Entry]
Type=Application
Exec=gnome-terminal -e /home/$USER/s5.sh
Terminal=true
X-GNOME-Autostart-enabled=true
" > /home/$USER/.config/autostart/s5.desktop
chmod 777 /home/$USER/.config/autostart/s5.desktop
echo "config autostart finished"

#config sudo
echo "$USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

#config s5 log and temp file 
mkdir -p log
DATE=$(date +%Y-%m-%d)
LOG=log/${opt}_${DATE}.log
cat /dev/null > ${LOG}
echo "1" > ${opt}_cycle.txt
echo "$LOG" > ${opt}_log.txt
chmod 777 -R ./*.sh ./log/*.log ./*.txt

echo "System will shutdown after "$interval" sec, then restart after "$s5timer" sec"
echo "*********************** S5 Cycle: 1 *************************" |tee -a ${LOG}
echo `date +%Y-%m-%d' '%H:%M:%S` "Going to S5 after "$interval" sec" |tee -a ${LOG}
sleep $interval

rtcwake -m off -s $s5timer >> ${LOG} 2>&1

s5.sh

#!/bin/bash
COUNT="999"
opt=s5
interval=30
s5timer=180
cycle=$(<${opt}_cycle.txt)
LOG=$(<${opt}_log.txt)

#update cycle
((cycle++))
echo "$cycle" > ${opt}_cycle.txt 

#keep wake
echo "*********************** S5 Cycle: $cycle *************************" |tee -a ${LOG}
echo `date +%Y-%m-%d' '%H:%M:%S` "Going to S5 after "$interval" sec" |tee -a ${LOG}
sleep $interval

sudo rtcwake -m off -s $s5timer >> ${LOG} 2>&1

s5_stop.sh

#!/bin/bash
USER=$(users)
opt=s5
rm -f /home/$USER/.config/autostart/s5.desktop
rm -f ${opt}_cycle.txt ${opt}_log.txt
sed -i '/NOPASSWD/d' /etc/sudoers

测试脚本使用

S3

./s3s4.sh s3 999      启动s3测试,运行999次

image-20221205155429129

S4

S4在内存和硬盘直接读写运行时映像,硬盘需要指定那一块区域用于和内存交换,即交换分区。Linux一切皆文件,交换分区也可以用swapfile配置。以下配置swapfile

  1. df –h 查看挂载点为/对应的文件系统是/dev/nvme0n1p2,根据你具体情况记录

  2. blkid查看UUID值,根据1对应的nvme文件系统记录UUID

  3. filefrag –v /swapfile查看swapfile的物理起始地址,记录physical_offset左侧值

  4. 将UUID和physical_offset值写入grub:
    终端输入gedit /etc/default/grub ,修改以下参数并保存
    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=UUID=你的UUID值 resume_offset=你的physical_offset值

    image-20221205155518922

  5. 重新生成grub: 终端输入 update-grub 回车并重启电脑。

启动S4脚本

./s3s4.sh s4 999

image-20221205155749113

S5

自动重启需要先解决账户密码问题
设置普通账户自动登录:
普通账户为装系统时设置的账户,重启后默认以普通账户登录
1.终端输入gedit /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf弹出编辑界面
设置以下参数,ctrl+s保存后关闭

[Seat:*]
user-session=ubuntu
autologin-user=你的账户名

2.终端输入gedit /etc/gdm3/custom.conf,设置以下几行的值为如下

# Enabling automatic login
AutomaticLoginEnable = true
AutomaticLogin =你的账户名

重启,确认可免密码登录桌面。

./s5_start.sh 启动s5
./s5_stop.sh 结束S5

Dmesg分析和调试

dmesg简介

dmesg命令显示linux内核的环形缓冲区信息,我们可以从中获得诸如系统架构、cpu、挂载的硬件,RAM等多个运行级别的大量的系统信息。当计算机启动时,系统内核(操作系统的核心部分)将会被加载到内存中。在加载的过程中会显示很多的信息,在这些信息中我们可以看到内核检测硬件设备
注意:
dmesg只记录从启动到当前时间的信息,掉电丢失

使用示例:

dmesg //默认输出
dmesg | less //从头分页显示
dmesg | tail -100 //显示最后100行
dmesg | head  -100 //显示最早100行
dmesg | grep -i usb //包含usb的信息,忽略大小写
dmesg -C //清除log

输出示例:

[root]# dmesg | grep sda
 
[    1.280971] sd 2:0:0:0: [sda] 488281250 512-byte logical blocks: (250 GB/232 GiB)
[    1.281014] sd 2:0:0:0: [sda] Write Protect is off
[    1.281016] sd 2:0:0:0: [sda] Mode Sense: 00 3a 00 00
[    1.281039] sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[    1.359585]  sda: sda1 sda2 < sda5 sda6 sda7 sda8 >
[    1.360052] sd 2:0:0:0: [sda] Attached SCSI disk
[    2.347887] EXT4-fs (sda1): mounted filesystem with ordered data mode. Opts: (null)
[   22.928440] Adding 3905532k swap on /dev/sda6.  Priority:-1 extents:1 across:3905532k FS
[   23.950543] EXT4-fs (sda1): re-mounted. Opts: errors=remount-ro
[   24.134016] EXT4-fs (sda5): mounted filesystem with ordered data mode. Opts: (null)
[   24.330762] EXT4-fs (sda7): mounted filesystem with ordered data mode. Opts: (null)
[   24.561015] EXT4-fs (sda8): mounted filesystem with ordered data mode. Opts: (null)

输出时间戳是从上电开始,到当前的时间,以秒为单位。

调试S4 hang

在测试中发现,nvme存储设备,首次进入S4的时间较长,约30s, 且系统处于hang状态,鼠标无法移动。
dmesg查看单次S4 enter & resume的过程,log如下

[   43.090180] PM: hibernation entry          //开机43秒进s4流程
[   43.090579] PM: Syncing filesystems ...   
[   43.090682] PM: done.
[   43.090684] Freezing user space processes ... (elapsed 0.003 seconds) done.   //冻结用户进程,这个时候会hang
[   43.093699] OOM killer disabled.
[   43.093876] PM: Marking nosave pages: [mem 0x00000000-0x00000fff]       //准备哪些内存要转存到disk
[   43.093878] PM: Marking nosave pages: [mem 0x0005f000-0x0005ffff]
[   43.093880] PM: Marking nosave pages: [mem 0x000a0000-0x000fffff]
[   43.093887] PM: Marking nosave pages: [mem 0x80185000-0x80186fff]
[   43.093889] PM: Marking nosave pages: [mem 0x88c4f000-0x890acfff]
[   43.093944] PM: Marking nosave pages: [mem 0x89214000-0x89efefff]
[   43.094102] PM: Marking nosave pages: [mem 0x89f00000-0xffffffff]
[   43.098151] PM: Basic memory bitmaps created
[   43.098541] PM: Preallocating image memory... 
[   43.143802] hpet_rtc_timer_reinit: 29 callbacks suppressed
[   43.143803] hpet1: lost 2 rtc interrupts
[   43.197779] hpet1: lost 2 rtc interrupts
[   43.251708] done (allocated 333315 pages)               //分配内存侧的交换页
[   43.251709] PM: Allocated 1333260 kbytes in 0.15 seconds (8888.40 MB/s)
[   43.251710] Freezing remaining freezable tasks ... (elapsed 0.126 seconds) done.
[   43.379023] printk: Suspending console(s) (use no_console_suspend to debug)
[   43.381268] serial 00:02: disabled
[   43.382187] parport_pc 00:01: disabled
[   43.701432] ACPI: Preparing to enter system sleep state S4  //准备进S4, 关CPU, 中断,
[   44.400195] PM: Saving platform NVS memory
[   44.404308] Disabling non-boot CPUs ...
[   44.404935] IRQ 123: no longer affine to CPU1
[   44.404942] IRQ 132: no longer affine to CPU1
[   44.405979] smpboot: CPU 1 is now offline
[   44.410695] smpboot: CPU 2 is now offline
[   44.414738] IRQ 122: no longer affine to CPU3
[   44.415784] smpboot: CPU 3 is now offline
[   44.422078] PM: Creating hibernation image:           //准备创建disk映像
[   44.503241] PM: Need to copy 330336 pages
[   44.503242] PM: Normal pages needed: 330336 + 1024, available pages: 1731140
                                                                                       //磁盘写入操作和时间没有记录
[   44.901845] PM: free pages cleared after restore    //S4已被唤醒,这里不是绝对时间,S4休眠时间未记入
[   44.901889] PM: Restoring platform NVS memory  
[   44.903440] Enabling non-boot CPUs ...
[   44.903474] x86: Booting SMP configuration:
[   44.903474] smpboot: Booting Node 0 Processor 1 APIC 0x2
[   44.904974]  cache: parent cpu1 should not be sleeping
[   44.905085] CPU1 is up
[   44.905101] smpboot: Booting Node 0 Processor 2 APIC 0x4
[   44.905478]  cache: parent cpu2 should not be sleeping
[   44.905602] CPU2 is up
[   44.905618] smpboot: Booting Node 0 Processor 3 APIC 0x6
[   44.905995]  cache: parent cpu3 should not be sleeping
[   44.906125] CPU3 is up
[   44.908816] ACPI: Waking up from system sleep state S4
[   45.003392] usb usb1: root hub lost power or was reset
[   45.003393] usb usb2: root hub lost power or was reset
[   45.006573] sd 0:0:0:0: [sda] Starting disk
[   45.006732] parport_pc 00:01: activated
[   45.008262] serial 00:02: activated
[   45.384656] ata1: SATA link up 3.0 Gbps (SStatus 123 SControl 300)
[   45.384717] ata2: SATA link down (SStatus 4 SControl 300)
[   45.384741] ata3: SATA link down (SStatus 4 SControl 300)
[   45.384758] ata6: SATA link down (SStatus 4 SControl 300)
[   45.384779] ata5: SATA link down (SStatus 4 SControl 300)
[   45.384798] ata4: SATA link down (SStatus 4 SControl 300)
[   45.387712] ata1.00: configured for UDMA/100
[   45.500662] usb 1-9: reset low-speed USB device number 2 using xhci_hcd
[   46.056539] usb 1-10: reset low-speed USB device number 3 using xhci_hcd
[   46.318112] nvme nvme0: 4/0/0 default/read/poll queues
[   46.336791] nvme nvme0: ctrl returned bogus length: 2 for NVME_NIDT_EUI64
[   46.363904] acpi LNXPOWER:07: Turning OFF
[   46.363916] acpi LNXPOWER:06: Turning OFF
[   46.364472] PM: Basic memory bitmaps freed
[   46.364474] OOM killer enabled.
[   46.364475] Restarting tasks ... done.   //恢复进程
[   47.098831] e1000e: eno1 NIC Link is Up 100 Mbps Full Duplex, Flow Control: None
[   47.098837] e1000e 0000:00:1f.6 eno1: 10/100 speed: disabling TSO
[   49.489104] video LNXVIDEO:00: Restoring backlight state
[   49.489109] PM: hibernation exit    //S4退出完成

比较奇怪的是,dmesg没记录写磁盘的操作,时间戳上也反映不出来。可能这时候IO交给DMA了,kernel就挂起了,所以dmesg无法工作,包括时间戳记录??
为了验证S4 hang原因在于写磁盘,对不同SSD做读写测速:

DISK               :         R/W speed MB/s   :       S4 hang time
Samsung SATA SSD   :         567/529          :       4s
Our SSD            :         170.1/104.7      :       25s

基本验证读写速度和S4 hang是线性关系。

初始化git和github仓库

1.安装git

2.进入本地源码目录

git init

会出现.git目录
首次需要配置github账户和邮箱

git config --global user.name "github注册的用户名"
git config --global user.mail "github注册的邮箱"

3.添加远程仓库

在github网页新建仓库

git remote add origin git@github.com:github用户名/仓库名.git

.git/config文件内容会出现remote等内容,ssh方式的url是git开头,http(s)方式是http(s)开头
image-20221205111653141
如果是从别人拉过来的仓库,修改后新建仓库,上传遇到fatal: remote origin already exists问题,解决方法:

git remote rm origin
git remote add origin git@github.com:github用户名/仓库名.git

4.git add, commit, push三连

git add -A
git commit -m 'first commit'
git push -f --set-upstream origin master //首次提交

image-20221205111703940
完成以后远程可以看得到仓库的文件

5.创建分支

如果已经有主线,在本地git checkout branchname, 远程创建分支,记录.git链接, 然后关联远程分支即可:

git remote add origin https://github.com/*/*.git

然后推送

git push origin branchname

首次配置可能的问题:

push时有RSA key错误

image-20221205111713234
因为Git使用SSH连接,而SSH第一次连接需要验证GitHub服务器的Key。确认GitHub的Key的指纹信息是否真的来自GitHub的服务器。解决办法是在本地生成key,配置到github服务器
(1)创建ssh key

ls -al ~/.ssh
ssh-keygen -t rsa -C "github用户名"
cat ~/.ssh/id_rsa.pub

image-20221205111721197
在push三连过程可以设置global全局配置,以后默认push到github
image-20221205111728996

(2)配置ssh key到github
登陆github,头像-settings-new SSH,复制新生成的SSH配置到服务器
image-20221205111737339
(3)需要重新add origin新建仓库(或者网页上新建仓库),再push,git statusgit log查看分支和日志

push时不能使用密码登陆

1
Support for password authentication was removed on August 13, 2021.

使用personal token替代密码登陆:

Github setting -> Developer setting -> Personal access token -> Generate a New Token (classic) -> 设置不过时,所有权限勾上 -> 首次会显示token字符,下次不会显示,记得备份token!-> 再次输入账号密码时用token代替密码即可push

git clone有HTTP2错误

错误码:RPC failed; curl 16 Error in the HTTP2 framing layer

解决办法:Git使用HTTP1.1

1
git config --global http.version HTTP/1.1

Github clone使用国内镜像

国内搞开发最痛苦的就是限速+断开连接,github clone经常失败。推荐国内镜像服务作为代理进行git clone,将原git地址的github.com替换成代理地址即可。参考 无需代理直接加速各种 GitHub 资源拉取

1
2
3
4
5
6
7
8
9
10
11
12
#git clone原地址
$ git clone https://github.com/kubernetes/kubernetes.git

#手动配置代理地址,任选其一能clone成功即可
$ git clone https://github.com.cnpmjs.org/kubernetes/kubernetes.git
$ git clone https://hub.fastgit.org/kubernetes/kubernetes.git
$ git clone https://gitclone.com/github.com/kubernetes/kubernetes.git

#配置git自动使用代理,配置以后可以用git clone原地址,自动走代理
git config --global url."https://hub.fastgit.org".insteadOf https://github.com
#取消自动代理
$ git config --global --unset url.https://github.com/.insteadof

使用国内镜像并不一定能解决所有clone问题,有的recursive clone对依赖包有版本要求,国内镜像版本不匹配导致clone fail,此时不能使用国内镜像。

解决版本:下载release版本的zip包,绕开git clone操作。

Github连接报错问题

OpenSSL errno 10054

一劳永逸的解决办法:git bash -> git config –global http.sslVerify “false”

参考:OpenSSL SSL_read: Connection was reset, errno 10054

背景

某软件有不同的配置参数,实现不同功能版本的编译
批量测试需要批量编译各种版本,实现方式为:
1.将编译参数组合,生成大量配置文件
2.编译过程遍历这些配置文件,依次编译对应版本
3.有参数加入,修改,删除,只需要更新这些配置文件
如何实现这些配置文件的更新?

实例

某芯片的Firmware批量编译实现:
Firmware代码为C, 配置参数用宏实现,后缀为.def
目录结构如下

|–project_folder
  |–config
   |–build.def
   |–defs
     |–1.def 2.def … n.def
  |–src
  |–Makefile
  |–build_All.sh
  |–update.sh

批量编译脚本

批量编译脚本如下
基本过程:
1.依次拷贝def文件夹中的每个def,替换默认的build.def
2.编译,接受所有编译参数
3.拷贝编译输出的image到包含git tag, def名,时间等信息的文件夹

#!/bin/bash

echo "Batch build support args:"
echo "1. functin version:"
echo "verargs=mp_fpga"
echo "verargs=mpw_asic"
echo "2. boot debug:"
echo "bootargs=debug"

OUTPUT=batch_build_$1$2

mkdir -p ${OUTPUT}
rm -rf ./batch_build_*

build_time=`date +%Y%m%d%H%M%S`

#commit_id=`git rev-parse HEAD`

tag_name=`git describe --exact-match --tags 2>/dev/null`

if [ -z "${tag_name}" ]; then
    tag_name="NO_TAG"
fi

mv ./config/build.def ./config/build.def.bak 

for file in `ls ./config/defs/*.def`;
do
    file_name=${file##*/}
    config_name=${file_name%.def}
    
    cp -rf ${file} ./config/build.def
    make clean
    make -j4 $@
    #mv ./build/image ./batch_build/${tag_name}_${config_name}_time_${build_time}_cid_${commit_id}
    mkdir -p ./${OUTPUT}/${tag_name}_${config_name}_time_${build_time}
    mv ./build/image/* ./${OUTPUT}/${tag_name}_${config_name}_time_${build_time}
done

mv ./config/build.def.bak ./config/build.def

def文件内的.def文件即各种参数配置文件,.def文件名即参数功能的别名组合
例如:

CQ_emmc_two_card_enhance_hs400_always_l0_MSIx_dllphase14_tuning_on.def

对应的内容是:

/*0: Non-CQ mode 1:CQ mode enable*/
#define BB_CQ_MODE_ENABLE 1
/*the card number support emmc#0:0 emmc#1:1 two card:2*/
#define BB_CARD_NUMBER 2
/*0:legacy 1:High Speed 50MHz 2:HS200 3:HS400 4:Enhance HS400*/
#define BB_MAX_TRANSFER_MODE 4
/* power mode management: 0: Low Power 1:Balance 2:High Performance 3:Direct HP 4:Always L0*/
#define POWER_MANAGEMENT_MODE 4
/* INT_MODE: 0:MSI_X 1:INTx 2:MSI_MULTIPLE 3:MSI_SINGLE */
#define INT_MODE 0
/* The selection of DLL PHASE COUNT is 11 or 14 */
#define DLL_phase_cnt 14
/* 0: fixed output phase  1: auto output tuning */
#define AUTO_OUTPUT_TUNING 1

批量编辑配置文件

配置文件def有两个属性
1.文件名每个词代表一个功能,各词用下划线“_”分隔
2.内部用宏定义实现功能配置,宏定义的值要和外部文件名匹配

基于以上属性,编辑脚本需求为:
1.新增:增加一个宏定义,并增加对应的功能缩写到文件名
2.修改:修改一个已存在的宏定义,并修改对应的功能缩写到文件名
3.删除:删除一个已存在的宏定义,并删除对应的功能缩写到文件名
4.其他功能,如直接删除含某缩写的文件,备份原配置文件

shell实现为update.sh,如下:

#!/bin/bash

DEFS_PATH="./config/defs"
DEFS_BACKUP_PATH="./config/defs_backup"
DEFS_TEMP_PATH="./config/defs_temp"

if [ $# -lt 1 ];then
        echo "usage: ./update.sh [option] [args]"

        echo "example 0:"
        echo "        backup defs files:"
        echo "        ./update.sh -bf"
        echo ""

        echo "example 1:"
        echo "        add a macro name and macro value to defs, and add file postfix:"
        echo "        ./update.sh -b"
        echo "        ./update.sh -a balance POWER_MANAGEMENT_MODE 1 "
        echo "        ./update.sh -a high_performance POWER_MANAGEMENT_MODE 2 "
        echo "        add other values..."
        echo ""

        echo "example 2:"
        echo "        update a macro name and macro value to defs, and update file postfix:"
        echo "        ./update.sh -u balance lowpower POWER_MANAGEMENT_MODE 1 "
        echo ""

        echo "example 3:"
        echo "        delete a macro name and macro value of defs, and delete file postfix:"
        echo "        ./update.sh -d lowpower POWER_MANAGEMENT_MODE"
        echo ""

        echo "example 4:"
        echo "        delete target files:"
        echo "        ./update.sh -df lowpower"
        echo ""

        echo "example 5:"
        echo "        clean backup defs files:"
        echo "        ./update.sh -cf"
        echo ""

        exit;
    fi

if [ $1 = "-bf" ];then #backup defs
    mkdir -p $DEFS_BACKUP_PATH
    mv $DEFS_PATH/*.def $DEFS_BACKUP_PATH

elif [ $1 = "-cf" ];then #clear backup defs
    rm -rf $DEFS_BACKUP_PATH


​ #add a macro name and macro value to defs, and add file postfix
​ elif [ $1 = “-a” ];then

if [ $# != 4 ];then
echo “usage: ./update.sh -a FILE_POSTFIX MACRO_NAME MACRO_VALUE”
exit;
fi

    mkdir -p $DEFS_TEMP_PATH && cp -rf $DEFS_BACKUP_PATH/*.def $DEFS_TEMP_PATH
    
    FILE_POSTFIX=$2
    MACRO_NAME=$3
    MACRO_VALUE=$4
    # sed -i makes change on original file, otherwise on stream
    # xargs transfer multiple output from stream to multiple args to sed
    find ${DEFS_TEMP_PATH} -name '*.def' | xargs sed -i '$a\#define\ '"$MACRO_NAME"'\ '"$MACRO_VALUE"''
    
    for file in `ls ${DEFS_TEMP_PATH}/*.def`
    do
     mv $file `echo $file | sed 's/\(.*\)\(\..*\)/\1_'"$FILE_POSTFIX"'\2/g'`
    done
    
    cp -rf $DEFS_TEMP_PATH/*.def $DEFS_PATH
    rm -rf $DEFS_TEMP_PATH

#update a macro name and macro value to defs, and update file postfix
elif [ $1 = "-u" ];then

    if [ $# != 5 ];then
        echo "usage: ./update.sh -u ORIGIN_POSTFIX UPDATED_POSTFIX MACRO_NAME MACRO_UPDATED_VALUE"
        exit;
    fi

    ORIGIN_POSTFIX=$2
    UPDATED_POSTFIX=$3
    MACRO_NAME=$4
    MACRO_UPDATED_VALUE=$5

    #replace all lines that pattern matches $MACRO_NAME
    find ${DEFS_PATH} -name '*.def' | grep $ORIGIN_POSTFIX | xargs sed -i 's/.*'"$MACRO_NAME"'.*/#define\ '"$MACRO_NAME"'\ '"$MACRO_UPDATED_VALUE"'/g'
    #update file postfix
    for file in `ls ${DEFS_PATH}/*$ORIGIN_POSTFIX*.def`
    do
         mv $file `echo $file | sed 's/'"$ORIGIN_POSTFIX"'/'"$UPDATED_POSTFIX"'/g'`
    done


​ #delete a macro name and macro value of defs, and delete file postfix
​ elif [ $1 = “-d” ];then

if [ $# != 3 ];then
echo “usage: ./update.sh -d DELETE_POSTFIX MACRO_NAME”
exit;
fi

    DELETE_POSTFIX=$2
    MACRO_NAME=$3
    #delete all lines that contain $MACRO_NAME
    find ${DEFS_PATH} -name '*.def' | grep $DELETE_POSTFIX | xargs sed -i '/'"$MACRO_NAME"'/d'
    #delete file postfix
    for file in `ls ${DEFS_PATH}/*.def`
    do
         mv $file `echo $file | sed 's/_'"$DELETE_POSTFIX"'//g'`
    done

#delete target file by postfix
elif [ $1 = "-df" ];then
    
    if [ $# != 2 ];then
        echo "usage: ./update.sh -df DELETE_POSTFIX"
        exit;
    fi

    DELETE_POSTFIX=$2
    rm -f ${DEFS_PATH}/*$DELETE_POSTFIX*.def

fi

重点讲下其中的几个sed和文件操作
1.多个文件,每个文件最后一行追加内容

find ${DEFS_TEMP_PATH} -name '*.def' | xargs sed -i '$a\#define\ '"$MACRO_NAME"'\ '"$MACRO_VALUE"''
  • xargs的作用: find -name 输出的是多个文件名,不能直接传给sed, xargs将多个文件名转化成多个参数,每个参数是一个文件名,sed可以接收
  • -i的作用:sed本身是在文件的拷贝上操作,不直接在文件本身修改,-i(insert)使sed的操作在文件上生效,如果不加-i,源文件不会被修改
  • $:表示最后一行,sed ‘a\string’是基础格式
  • 注意sed怎么用带空格和变量的字符串:空格用转义’\ ‘表示,变量是单引号内加双引号,即’”$ARG”‘

2.找到包含字符串A的所有文件,替换内容:将字符串B替换为C

#replace all lines that pattern matches $MACRO_NAME
find ${DEFS_PATH} -name '*.def' | grep $ORIGIN_POSTFIX | xargs sed -i 's/.*'"$MACRO_NAME"'.*/#define\ '"$MACRO_NAME"'\ '"$MACRO_UPDATED_VALUE"'/g'
  • find | grep 是常用套路,先找在过滤,注意find -name 可以使用*, grep不要用*,否则grep会把它当成要匹配的字符
  • sed ‘s/stringB/stringC’是基础格式,g表示全局,注意要-i

3.找到包含字符串A的所有文件,删除内容:包含字符串B的行

#delete all lines that contain $MACRO_NAME
find ${DEFS_PATH} -name '*.def' | grep $DELETE_POSTFIX | xargs sed -i '/'"$MACRO_NAME"'/d'

4.对多个文件的文件名,增加,修改,删除特定字符串

#在原文件名最后,后缀之前,增加字符串“_$FILE_POSTFIX”
for file in `ls ${DEFS_TEMP_PATH}/*.def`
    do
     mv $file `echo $file | sed 's/\(.*\)\(\..*\)/\1_'"$FILE_POSTFIX"'\2/g'`
    done

#替换文件名中匹配的字符
for file in `ls ${DEFS_PATH}/*$ORIGIN_POSTFIX*.def`
do
     mv $file `echo $file | sed 's/'"$ORIGIN_POSTFIX"'/'"$UPDATED_POSTFIX"'/g'`
done

#删除文件名指定字符
for file in `ls ${DEFS_PATH}/*.def`
do
     mv $file `echo $file | sed 's/_'"$DELETE_POSTFIX"'//g'`
done
  • for < args > in `ls < path >`是把多个文件名依次写入变量args的常用操作,类似于xargs,不过是用循环每次处理一个文件名变量
  • mv $file `echo $file | sed ‘s/stringA/stringB/g’`实际是两个步骤:先用echo把当前处理的文件名传给sed, sed处理完的输出文件名,作为mv的目标文件名,覆盖了原文件
  • 注意,文件名xargs传给sed,处理的是文件内容,for-in-do一个个mv,才是处理文件名本身

相关文章

使用 sed 命令查找和替换文件中的字符串的 16 个示例
sed引入变量的几种方法
sed 批量替换文件内容

gcc编译选项

gcc提供了大量的警告选项,对代码中可能存在的问题提出警告,通常可以使用-Wall来开启以下警告:

   -Waddress -Warray-bounds (only with -O2) -Wc++0x-compat
   -Wchar-subscripts -Wimplicit-int -Wimplicit-function-declaration
   -Wcomment -Wformat -Wmain (only for C/ObjC and unless
   -ffreestanding) -Wmissing-braces -Wnonnull -Wparentheses
   -Wpointer-sign -Wreorder -Wreturn-type -Wsequence-point
   -Wsign-compare (only in C++) -Wstrict-aliasing -Wstrict-overflow=1
   -Wswitch -Wtrigraphs -Wuninitialized (only with -O1 and above)
   -Wunknown-pragmas -Wunused-function -Wunused-label -Wunused-value
   -Wunused-variable

unused-function:警告声明但是没有定义的static函数;
unused- label:声明但是未使用的标签;
unused-parameter:警告未使用的函数参数;
unused-variable:声明但是未使用的本地变量;
unused-value:计算了但是未使用的值;
format:printf和scanf这样的函数中的格式字符串的使用不当;
implicit-int:未指定类型;
implicit-function:函数在声明前使用;
char- subscripts:使用char类作为数组下标(因为char可能是有符号数);
missingbraces:大括号不匹配;
parentheses: 圆括号不匹配;
return-type:函数有无返回值以及返回值类型不匹配;
sequence-point:违反顺序点的代码,比如 a[i] = c[i++];
switch:switch语句缺少default或者switch使用枚举变量为索引时缺少某个变量的case;
strict- aliasing=n:使用n设置对指针变量指向的对象类型产生警告的限制程度,默认n=3;只有在-fstrict-aliasing设置的情况下有效;
unknow-pragmas:使用未知的#pragma指令;
uninitialized:使用的变量为初始化,只在-O2时有效;

以下是在-Wall中不会激活的警告选项:

cast-align:当指针进行类型转换后有内存对齐要求更严格时发出警告;
sign- compare:当使用signed和unsigned类型比较时;
missing-prototypes:当函数在使用前没有函数原型时;
packed:packed 是gcc的一个扩展,是使结构体各成员之间不留内存对齐所需的空间,有时候会造成内存对齐的问题;
padded:也是gcc的扩展,使结构体成员之间进行内存对齐的填充,会造成结构体体积增大.
unreachable-code:有不会执行的代码时.
inline:当inline函数不再保持inline时 (比如对inline函数取地址);
disable-optimization:当不能执行指定的优化时.(需要太多时间或系统资源).
可以使用 -Werror时所有的警告都变成错误,使出现警告时也停止编译.需要和指定警告的参数一起使用.

编译的优化级别:
gcc默认提供了5级优化选项的集合:

-O0:无优化(默认)
-O和-O1:使用能减少目标文件大小以及执行时间并且不会使编译时间明显增加的优化.在编译大型程序的时候会显著增加编译时内存的使用.
-O2: 包含-O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化.编译器不执行循环展开以及函数内联.此选项将增加编译时间和目标文件的执行性能.
-Os:专门优化目标文件大小,执行所有的不增加目标文件大小的-O2优化选项.并且执行专门减小目标文件大小的优化选项.
-O3: 打开所有-O2的优化选项并且增加 -finline-functions, -funswitch-loops,-fpredictive-commoning, -fgcse-after-reload and -ftree-vectorize优化选项.

-O1包含的选项-O1通常可以安全的和调试的选项一起使用:

   -fauto-inc-dec -fcprop-registers -fdce -fdefer-pop -fdelayed-branch
   -fdse -fguess-branch-probability -fif-conversion2 -fif-conversion
   -finline-small-functions -fipa-pure-const -fipa-reference
   -fmerge-constants -fsplit-wide-types -ftree-ccp -ftree-ch
   -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse
   -ftree-fre -ftree-sra -ftree-ter -funit-at-a-time

以下所有的优化选项需要在名字前加上-f,如果不需要此选项可以使用-fno-前缀

defer-pop:延迟到只在必要时从函数参数栈中pop参数;
thread- jumps:使用跳转线程优化,避免跳转到另一个跳转;
branch-probabilities:分支优化;
cprop- registers:使用寄存器之间copy-propagation传值;
guess-branch-probability:分支预测;
omit- frame-pointer:可能的情况下不产生栈帧;

-O2:以下是-O2在-O1基础上增加的优化选项:

    -falign-functions  -falign-jumps -falign-loops  -falign-labels
   -fcaller-saves -fcrossjumping -fcse-follow-jumps  -fcse-skip-blocks
   -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
   -fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove
   -freorder-blocks  -freorder-functions -frerun-cse-after-loop
   -fsched-interblock  -fsched-spec -fschedule-insns
   -fschedule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
   -ftree-vrp

cpu架构的优化选项,通常是-mcpu(将被取消);-march,-mtune

Debug选项:

在 gcc编译源代码时指定-g选项可以产生带有调试信息的目标代码,gcc可以为多个不同平台上帝不同调试器提供调试信息,默认gcc产生的调试信息是为 gdb使用的,可以使用-gformat 指定要生成的调试信息的格式以提供给其他平台的其他调试器使用.常用的格式有
-ggdb:生成gdb专用的调试信息,使用最适合的格式(DWARF 2,stabs等)会有一些gdb专用的扩展,可能造成其他调试器无法运行.
-gstabs:使用 stabs格式,不包含gdb扩展,stabs常用于BSD系统的DBX调试器.
-gcoff:产生COFF格式的调试信息,常用于System V下的SDB调试器;
-gxcoff:产生XCOFF格式的调试信息,用于IBM的RS/6000下的DBX调试器;
-gdwarf- 2:产生DWARF version2 的格式的调试信息,常用于IRIXX6上的DBX调试器.GCC会使用DWARF version3的一些特性.

可以指定调试信息的等级:在指定的调试格式后面加上等级:
如: -ggdb2 等,0代表不产生调试信息.在使用-gdwarf-2时因为最早的格式为-gdwarf2会造成混乱,所以要额外使用一个-glevel来指定调试信息的等级,其他格式选项也可以另外指定等级.
gcc可以使用-p选项指定生成信息以供porf使用.

gcc配置选项

6

gcc常用选项

1

2

3

4

5

文件名替换

1.wildcard
展开多个文件为使用空格分开的、匹配此模式的列表参数
格式
$(wildcard PATTERN...)

示例:

SRC=$(wildcard *.c)

2.patsubst
替换通配符
格式

$(patsubst %.c,%.o,$(dir))

示例:

obj := $(patsubst %.c,%.o,$(wildcard *.c))

3.替换引用
patsubst的示例等价于:

obj=$(dir:%.c=%.o)

引用替换:

$(var:a=b) 或 ${var:a=b}

含义是把变量var中的每一个值,用b替换掉a

PHONY

Makefile执行的规则是A:B,表示A依赖于B

  • 有B才能执行A对应的编译操
  • B有更新(检测文件时间),则A会执行,若B没有更新,不执行A

问题来了,clean: 不需要依赖任何对象,如何执行
PHONY定义伪目标,可以解决源文件不是最终目标直接依赖(即间接依赖)带来的不能自动检查更新规则, 示例如下

.PHONY: clean
clean:
    rm -f *.o

PHONY不仅用于clean,对于间接依赖也有用,例如A:B, B:C ,有时C为空,就需要把B定义为PHONY

OBJS = *.o
program:  $(OBJS)
        gcc *.o -o program
 
.PHONY : $(OBJS)
$(OBJS):
        make -C $(dir $@)

不过一般情况,obj依赖于%.c,总之,对于没有依赖项的对象,需要定义为PHONY

通配符

常见通配符

$@, $^, $<, $?

$@  表示目标文件
$^  表示所有的依赖文件
$<  表示第一个依赖文件
$?  表示比目标还要新的依赖文件列表

示例:
编译Test目录下的.cpp文件,输出test可执行程序
直接指定依赖文件名的makefile写法:

test: $(wildcard Test/*.cpp)
    $(CXX) $(CFLAGS) -o test $(wildcard Test/*.cpp) 

虽然wildcard实现了所有依赖的.cpp的通配,编译语句再用wildcard写一次依赖文件不优雅,而且test在依赖中已经写了,编译语句又写一遍 -o test。
编译语句使用通配, 称为通用格式:

test: $(wildcard Test/*.cpp)
    $(CXX) $(CFLAGS) -o $@ $^

多个源文件分别编译

目录下有很多源文件,每个单独编译和执行的,将每一个文件编译成可执行文件,不用单独命名如:gcc -c xxx.c -o xxx
(1)Makefile实现

SRC=$(wildcard *.c)
OBJ=$(SRC:%.c=%.o)
BIN=$(OBJ:%.o=%)
 
CC=gcc
CFLAGS=-Wall -g -c
 
all:$(BIN)

$(BIN):%:%.o
        $(CC) $^ -o $@
$(OBJ):%.o:%.c
        $(CC) $(CFLAGS) $^ -o $@

.PHONY: clean
clean:
        rm -rf $(OBJ) $(BIN)

(2)Shell实现

#! /bin/bash
for file in ./*.c
do
if [ -f $file ]
then
file=${file#./}
target=${file%.c}
gcc -o $target $file
echo $target
fi
if [ -d $file ]
then
echo $file is mu lu
fi
done

(2)Makefile编译指定目录
Makefile可以输入参数,直接在make命令的后面加上参数,如:

make BUILD_DIR=./foldername/

传入的变量将会覆盖相应Makefile中的BUILD_DIR

shell增删改查概述

Linux Shell环境对文本增删改查,可以通过sed,awk和grep命令完成。

  • sed: 支持特定模式匹配的增加、插入、删除、替换、提取等操作
  • awk: 支持特定模式匹配的过滤查找,如提取某行列的字符串
  • grep: 支持包含特定字符串的结果提取,可配合find和awk实现过滤查找

这几种命令都支持流输入和文本输入,支持管道传递参数,也可以命令行传参。对于多个步骤的处理,可以几个命令串行使用。

shell的输入参数概述

Shell的命令,如cat, echo, sed, awk, grep, 管道命令|等,都要有输入参数,即待处理的数据。
输入参数有两种类型:

  • 标准输入:本质是stdin文件,即读取该文件的内容作为命令的输入参数,该文件内容可由其他命令的输出,或者用户输入来产生
  • 命令行输入:直接接受用户在命令行输入的字符串,一次性使用,不存储在stdin

支持标准输入作为参数的命令:cat, sed, awk, grep, |
只支持命令行输入字符串的命令:echo, ls
标准输入示例:

cat /etc/passwd | grep root

上面的代码使用了管道命令|,管道命令的作用是将左侧命令cat /etc/passwd的标准输出转换为标准输入,提供给右侧命令grep root作为参数。
以上命令也可以写成命令行输入形式:

grep root /etc/passwd

不支持标准输入的示例:

echo "hello world" | echo

输出为空,管道右侧的echo不接受管道传来的标准输入作为参数。
xargs的作用:将标准输入转为命令行参数

echo "hello world" | xargs echo

输出“hello world”,xargs和管道配合使用,能使管道也支持非标准输入命令。
xargs支持一些选项做进一步细化处理,如-p(打印), -d(分割) 等。

sed命令

sed命令概述

sed支持文本编辑,实现增、删、改的功能。
sed命令格式:

sed [options] 'command' filename

sed的输入参数可以用命令行,管道和xargs传入:

//命令行传入文件名参数
sed [options] 'command' filename 
//管道传入文件名参数
cat filename | sed [options] 'command'
//xargs传入文件名参数
cat filename | xargs sed [options] 'command'

sed对文件的编辑在缓冲区,不直接修改文本。要直接修改文本有以下方法:

  • 重定向覆盖文本, sed - x 'XXX' file.txt > file.txt
  • 特定的sed命令支持直接修改文本,如sed -i 'XXX' file.txt

sed的常用选项:

-n :关闭默认输出,只显示匹配的行
-i :直接修改读取的文件内容,而不是输出到终端。
-e :直接在命令列模式上进行sed的动作编辑;
-f :直接将sed的动作写在一个文件内,-f filename 则可以运行filename内的sed动作;
-r :启用扩展的正则表达式

sed的常用命令:

a :新增行,在指定行的后面附加一行,[address]a\新文本内容
i :插入行,在指定行的前面插入一行,[address]i\新文本内容
s :替换特定模式匹配的字符串, [address]s/pattern/replacement/flags
c :将指定行中的所有内容,替换成该选项后面的字符串, [address]c\用于替换的新文本
d :删除行,[address]d
p :打印, 通常与参数 -n 一起用,[address]p
w : 将文本中指定行的内容写入文件, [address]w filename

sed命令详解

本节从sed文本操作的“增删改查”举例说明其具体命令用法

新增和插入:a和i

sed的命令a和i都能实现新增行,其区别在于:

  • a :append, 指定行后面新增一行
  • i : insert, 表示在指定行前面插入一行

注意区分i命令和i选项
a和i命令的基本格式完全相同:

[address]a(或 i)\新文本内容

将一个新行插入到数据流第三行前:

sed '3i\This is an inserted line.' data6.txt

This is line number 1.
This is line number 2.
This is an inserted line.
This is line number 3.
This is line number 4.

将一个新行附加到数据流中第三行后:

sed '3a\This is an appended line.' data6.txt

This is line number 1.
This is line number 2.
This is line number 3.
This is an appended line.
This is line number 4.

将一个多行数据添加到数据流中,只需对要行末尾添加反斜线即可,类似C语言的代码换行

sed '1i\
This is one line of new text.\
This is another line of new text.' data6.txt

This is one line of new text.
This is another line of new text.
This is line number 1.
This is line number 2.
This is line number 3.
This is line number 4.

删除:d

  • d: delete, 删除行

格式:

[address]d

删除第三行:

[root@localhost ~]# cat data6.txt
This is line number 1.
This is line number 2.
This is line number 3.
This is line number 4.
[root@localhost ~]# sed '3d' data6.txt
This is line number 1.
This is line number 2.
This is line number 4.

删除二、三行:

sed '2,3d' data6.txt
This is line number 1.
This is line number 4.

删除第三行开始的后续所有行:

[root@localhost ~]# sed '3,$d' data6.txt
This is line number 1.
This is line number 2.

注意:sed d 命令并不会修改原始文件,这里被删除的行只是从 sed 的缓冲区中消失了,原始文件没做任何改变,若要修改源文件,可重定向sed的输出到源文件,覆盖原内容使修改生效。

匹配定位:/pattern/

sed的增删操作,可以针对包含匹配字符串的行进行操作,其原理是活用[address], 支持匹配字符串的定位,以插入命令’i’为例,匹配格式如下:

sed [option] '/匹配字符串/i \插入字符串'
[option] 通常为 -i, 修改直接在源文件生效

原文件:

cat testfile 
hello

在包含”hello”的一行的上一行,插入”upline”:

sed -i '/hello/i\upline' testfile

“hello”下一行插入”upline”:

sed -i '/hello/a\down' testfile

修改后的文件:

cat testfile 
up
hello
down

删除匹配到”hello”的行:

sed -i '/hello/d' testfile

如果匹配字符串有“/”,为了和sed命令的分隔符“/”,使用“\”转义。
例如删除匹配某个路径字符串的行:

匹配"\etc\install.sh"
set -i '/\/etc\/install.sh/d' test.txt

sed 命令包含一些预定义特殊符号,代表行尾,行首等。
删除以A开头的行:

sed -i '/^A.*/d' test.txt
^A表示开头是A, .*表示后跟任意字符串

在行尾追加一行内容:

sed -i '$a\added-content' test.txt
$表示定位到行尾,a是追加命令,added-content是追加内容

替换修改: s

s替换命令内部格式为:

[address]s/pattern/replacement/flags
  • address 指定要操作的具体行
  • pattern 指定需要替换的内容
  • replacement 指定替换的新内容
  • flags 指定特殊功能

常用的flags:

  • n 1~512 之间的数字,表示指定要替换的字符串出现第几次时才进行替换
  • g 对数据中所有匹配到的内容进行替换,否则只会在第一次匹配成功时做替换操作
  • p 会打印与替换命令中指定的模式匹配的行。此标记通常与 -n 选项一起使用
  • \ 转义(转义替换部分包含:&、\ 等)。

替换每行第二个匹配字符串:

sed 's/test/trial/2' data.txt
原文本:
This is first test of the test script
This is second test of the test script
输出:
This is first test of the trial script
This is second test of the trial script

只替换第二行的匹配字符串:

sed '2s/test/trial/' data.txt
原文本:
This is first test of the test script
This is second test of the test script
输出:
This is first test of the test script
This is second test of the trial script

全局替换所有匹配字符串:

sed 's/test/trial/g' data.txt
原文本:
This is first test of the test script
This is second test of the test script
输出:
This is first trial of the trial script
This is second trial of the trial script

提取:p

sed p命令配合字符串匹配,可以输出包含指定字符串的行内容。

sed -n '/string/p' filename
提取filename文件中,所有包含string的行的内容,并打印到标准输出
-n是只打印匹配命中的内容

sed p和grep都能提取内容,其区别在于:

  • sed '/string/p'是提取指定文件的行内容,重点在内容提取
  • grep "string" path是输出包含指定内容的所有文件路径,重点在查找文件位置

image-20221205145238133

image-20221205145248149

sed进阶与实战

多文件批量追加和删除

背景介绍:
底层固件代码有一些功能由宏定义控制,不同功能需要不同的宏定义组合,因此有不同的宏定义文件,命名为.def后缀。批量编译不同版本,编译脚本依次提取def文件夹内的.def文件和源代码一起编译。单个def内容如下:

image-20221205145321718

每一个宏都有多个取值,因此组合起来,需要生成一堆.def文件,批量编译才能覆盖各自功能。
每新增一个宏,都要修改所有.def文件,如果这个宏有两个取值,.def文件数量将翻倍。

image-20221205145327892

人工修改过于低效,使用sed可解决此问题。

查找指定文件,并批量追加一行内容:

find . -name '*.def*' | xargs sed -i '$a\added-content'

各命令含义:

find [path] -name "*.def"
查找path路径下,以.def结尾的所有文件,结果存储在stdout
|
管道,将查找结果转存到标准输入stdin
xargs
查找结果有很多个,用xargs转成命令行输入,sed才能批量处理
sed -i '$a\added-content'
    -i 直接修改文件,'$a\added-content' 最后一行追加added-content

查找指定文件,并批量删除匹配某字符串的行:

find ./defs -name '*.def' | xargs sed -i "/deleteString/d"

查找指定文件,并批量替换匹配某字符串:

find ./defs -name '*.def' | xargs sed -i "s/oldString/newString/"

在实际shell脚本中,通常由用户输入变量,$1, $2, $@ 解析变量。a、i、d和s能否在命令中使用双引号解析变量,实现动态编辑?
实验如下:

ARGS="AA BB"
find ./defs -name '*.def' | xargs sed -i "$a\${ARGS}"
find ./defs -name '*.def' | xargs sed -i "$i\${ARGS}"
find ./defs -name '*.def' | xargs sed -i "/${ARGS}/d"
find ./defs -name '*.def' | xargs sed -i "s/aabb/$ARGS/"
  • i 和 a 命令不能解析变量,实际追加的就是是${ARGS}
  • d命令可以解析变量,实际删除的是有”AA BB”的行
  • s命令可以解析变量,实际替换后的结果是”AA BB”

结论:将命令中的单引号改成双引号,理论上可以解析$包含的变量,实际上有的命令支持,有的命令不支持。

提取文件中的关键内容

背景介绍:
底层固件需要将代码段、数据段等关键信息存储在固件头部,编译过程中用编译器的size命令可以获取这些信息,并存储到文本,但是固件头部需要按自己的格式存储起始地址和大小信息,因此需要用sed提取并编辑该文本。

原文本:
image-20221205145348933

提取后文本:

sed命令:

sed -n '/string/p' oldFile | awk '{print $3}' >> newFile
提取oldFile内包含string的行,并用awk提取第三列,再写入newFile

该命令在Makefile实现,需要根据Makefile和shell特性做修改:

  • @:编译过程隐藏命令输出,类似于后台执行
  • $(shell xxxx): Makefile执行shell命令
  • $$: Makefile不能直接用shell的“$”解析变量,用“$$”

image-20221205145409877

背景

nodejs服务可以用nohup node xxx.js &后台启动,但是实际使用发现不太稳定,使用专门的node后台服务管理工具:pm2解决此问题

pm2特性

1、内建负载均衡(使用Node cluster 集群模块)
2、后台运行
3、0秒停机重载
4、具有Ubuntu和CentOS 的启动脚本
5、停止不稳定的进程(避免无限循环)
6、控制台检测
7、提供 HTTP API
8、远程控制和实时的接口API ( Nodejs 模块,允许和PM2进程管理器交互 )

pm2安装

npm install -g pm2

pm2用法

pm2 start app.js        //启动进程
pm2 start app.js -i 4   // 后台运行pm2,启动4个app.js
pm2 start app.js -i max //启动,使用所有CPU核心
pm2 start app.js --name my-api // 命名进程
pm2 list               // 显示所有进程状态
pm2 monit              // 监视所有进程
pm2 logs               //  显示所有进程日志
pm2 stop all           // 停止所有进程
pm2 restart all        // 重启所有进程
pm2 reload all         // 0秒停机重载进程 (用于 NETWORKED 进程)
pm2 stop 0             // 停止指定的进程
pm2 restart 0          // 重启指定的进程
pm2 startup            // 产生 init 脚本 保持进程活着
pm2 web                // 运行健壮的 computer API endpoint 
pm2 delete 0           // 杀死指定的进程
pm2 delete all         // 杀死全部进程

使用示例

部署Github项目NodeMail,每天给指定邮箱发邮件。

后台启动main.js并监测状态:

image-20221206142729375

image-20221206142743066

image-20221206142752627

image-20221206142800236

概述

shell中if-then-else-fi判断语句如下:

a="abc"

if [ $a = "abc" ]
then
   echo "$a = $b"
else
   echo "$a != $b"
fi

注意以下几点:

  • shell中的等号:=可用于赋值,也可以用于判断;==只用于判断,更规范
  • shell中的if语句各符号间都要空格分隔:if[ ]之间要空格;[ ]“ ”之间要空格; "=之间要空格。否则if语句中的符号会解析失败。
  • shell变量没有数据类型的区分,把任何存储在变量中的值,皆视为“字符串”
  • 对于变量可能为空的情况,需要用双括号[[ $a = "abc" ]]
  • if-then可以写在同一行,用;分隔两个语句:if [ $a = "abc" ];then

不同类型的判断语句

关系运算符判断

-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。

-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。

-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。

-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。

-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。

-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

#!/bin/bash

a=10
b=20

if [ $a -eq $b ]
then
   echo "$a -eq $b : a 等于 b"
else
   echo "$a -eq $b: a 不等于 b"
fi
if [ $a -ne $b ]
then
   echo "$a -ne $b: a 不等于 b"
else
   echo "$a -ne $b : a 等于 b"
fi
if [ $a -gt $b ]
then
   echo "$a -gt $b: a 大于 b"
else
   echo "$a -gt $b: a 不大于 b"
fi
if [ $a -lt $b ]
then
   echo "$a -lt $b: a 小于 b"
else
   echo "$a -lt $b: a 不小于 b"
fi
if [ $a -ge $b ]
then
   echo "$a -ge $b: a 大于或等于 b"
else
   echo "$a -ge $b: a 小于 b"
fi
if [ $a -le $b ]
then
   echo "$a -le $b: a 小于或等于 b"
else
   echo "$a -le $b: a 大于 b"
fi

布尔和逻辑运算符判断

! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。

-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。

-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

#!/bin/bash

a=10
b=20

if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a == $b: a 等于 b"
fi
if [ $a -lt 100 -a $b -gt 15 ]
then
   echo "$a 小于 100 且 $b 大于 15 : 返回 true"
else
   echo "$a 小于 100 且 $b 大于 15 : 返回 false"
fi
if [ $a -lt 100 -o $b -gt 100 ]
then
   echo "$a 小于 100 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 100 或 $b 大于 100 : 返回 false"
fi
if [ $a -lt 5 -o $b -gt 100 ]
then
   echo "$a 小于 5 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 5 或 $b 大于 100 : 返回 false"
fi

&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false

|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true

#!/bin/bash

a=10
b=20

if [[ $a -lt 100 && $b -gt 100 ]]
then
   echo "返回 true"
else
   echo "返回 false"
fi

if [[ $a -lt 100 || $b -gt 100 ]]
then
   echo "返回 true"
else
   echo "返回 false"
fi

字符串运算符判断

= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。

!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。

-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。

-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n “$a” ] 返回 true。

$ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

#!/bin/bash

a="abc"
b="efg"

if [ $a = $b ]
then
   echo "$a = $b : a 等于 b"
else
   echo "$a != $b: a 不等于 b"
fi
if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a = $b: a 等于 b"
fi
if [ -z $a ]
then
   echo "-z $a : 字符串长度为 0"
else
   echo "-z $a : 字符串长度不为 0"
fi
if [ -n "$a" ]
then
   echo "-n $a : 字符串长度不为 0"
else
   echo "-n $a : 字符串长度为 0"
fi
if [ $a ]
then
   echo "$a : 字符串不为空"
else
   echo "$a : 字符串为空"
fi

文件检查运算符判断

b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。

-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。

-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。

-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。

-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。

-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。

-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。

-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。

-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。

-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。

-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。

-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。

-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。

-S: 判断某文件是否 socket。
-L: 检测文件是否存在并且是一个符号链接。

#!/bin/bash

file="/root/test.sh"

if [ -r $file ]
then
   echo "文件可读"
else
   echo "文件不可读"
fi
if [ -w $file ]
then
   echo "文件可写"
else
   echo "文件不可写"
fi
if [ -x $file ]
then
   echo "文件可执行"
else
   echo "文件不可执行"
fi
if [ -f $file ]
then
   echo "文件为普通文件"
else
   echo "文件为特殊文件"
fi
if [ -d $file ]
then
   echo "文件是个目录"
else
   echo "文件不是个目录"
fi
if [ -s $file ]
then
   echo "文件不为空"
else
   echo "文件为空"
fi
if [ -e $file ]
then
   echo "文件存在"
else
   echo "文件不存在"
fi

判断语句报错:”unary operator expected”

在匹配字符串相等时,用了类似这样的语句:

if [ $STATUS == "OK" ]; then     
echo "OK"
fi

在运行时出现了 [: =: unary operator expected 的错误

if [[ $STATUS == "OK" ]]; 
then     
echo "OK"
fi

究其原因,是因为如果变量STATUS值为空,那么就成了 [ = “OK”] ,显然 [ 和 “OK” 不相等并且缺少了 [ 符号,所以报了这样的错误。当然不总是出错,如果变量STATUS值不为空,程序就正常了,所以这样的错误还是很隐蔽的。
或者用下面的方法也能避免这种错误:

if [ "$STATUS"x == "OK"x ]; 
then     
echo "OK"
fi

当然,x也可以是其他字符。shell中有没有双引号在很多情况下是一致的,因此x可以不加引号。

环境

阿里云ECS, CentOS7, RAM 4G

安装Gitlab

1.安装ssh并配置

#安装
sudo yum install -y curl policycoreutils-python openssh-server
#配置开机启动
sudo systemctl enable sshd
#启动服务
sudo systemctl start sshd

2.配置防火墙

#启动防火墙
service firewalld start
#添加http服务到firewalld,pemmanent表示永久生效
sudo firewall-cmd --permanent --add-service=http
#重启防火墙
sudo systemctl reload firewalld

3.安装gitlab

#下载安装脚本
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash
#安装
yum install -y gitlab-ee

4.配置gitlab

#gitlab配置文件
vim /etc/gitlab/gitlab.rb
#修改以下内容为主机ip和未使用的端口,否则使用默认端口8080
external_url 'http://47.100.221.149:9030'

5.配置生效并重启gitlab

#配置生效,改了配置需要运行
gitlab-ctl reconfigure
#重启服务,没改配置直接重启
gitlab-ctl restart

image-20221206144455394
似乎服务都正常启动了,实际上可能有各种问题,参考问题记录

问题Debug记录

按以上步骤配置好后,访问主机ip:端口,直接弹出502服务端错误
image-20221206144542817

配置文件权限问题?

配置文件生效命令gitlab-ctl reconfigure做了以下事情:

  • 配置设置写到gitlab服务直接调用的文件

实际生效的配置文件:

vim /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
vim /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml

image-20221206144606669
可见配置被自动拷贝到此处,gitlab相关服务读取的是这里的配置项

  • 生成服务相关临时文件

image-20221206144619066

原因:gitlab服务的配置文件在reconfigure时生成于/var/log/gitlab,这个文件目录默认权限不够,有些子服务不能正常运行。

解决方法:

chmod -R 777 /var/log/gitlab

restart服务,网页即可正常访问gitlab,如果恢复该目录755权限,重启服务会502

每次重新配置,gitlab-ctl reconfigure似乎会删除该目录再重新写入
image-20221206144636079

因此每次gitlab-ctl reconfigure之后都要chmod 777改此目录权限

还有502问题?

检查阿里云端口

首先确保主机ip是公网能访问的,不是内网ip
其次看端口是否禁用。在阿里云ECS的安全组策略中查看端口是否允许TCP输入输出
我把所有端口(1~65535)全部打开了
image-20221206144656411

检查前向端口冲突

gitlab配置文件的external_url就包含前向端口

netstat -nlp | grep 9030 (我的gitlab前向端口)

显示的是nginx服务端口,因为gitlab默认被nginx反向代理了,说明gitlab的代理服务确实占用9030。

image-20221206144927508

检查子服务的端口

注意gitlab不只有一个服务,默认配置只设置了前向端口(nginx代理端口),而子服务都没配置端口,默认用了8080,如果有其他服务已经占用8080,可能子服务起不来
例如unicorn子服务:

image-20221206144946178

查看子服务状态

gitlab-ctl status

image-20221206144958510

如果本地已有8080的服务,最好杀掉或换其他端口,否则要手动配置gitlab子服务端口

unicorn['port'] = 9032 (随便一个未使用端口)
gitlab_workhorse['auth_backend'] = "http://localhost:9032"

image-20221206145014236

检查内存资源不足

阿里云2G以下RAM可能会有因内存不足,导致服务不能正常启动。
使用swap交换分区,避免内存不足的可能性。swap分区可以把部分内存分页存储到磁盘,变相“扩大”内存

#查看现有swap分区,若未分配大小为0
cat /proc/swaps
#创建swap文件,大小4G,挂载到/mnt。交换分区可以使用真正的分区,或者以文件形式的分区
dd if=/dev/zero of=/mnt/swap bs=512 count=8388616
#使之成为swap分区
mkswap /mnt/swap
#修改swap分区配置
cat /proc/sys/vm/swappiness
sysctl -w vm.swappiness=60
#swap分区配置永久生效
vim /etc/sysctl.conf
修改vm.swappiness=60
#启动分区
swapon /mnt/swap
echo “/mnt/swap swap swap defaults 0 0” >> /etc/fstab
#停用分区
swapoff /mnt/swap
swapoff -a > /dev/null

启用分区后运行gitlab,发现已经有一部分数据转移到了swap文件
image-20221206145023389

ssh访问配置

通过ssh上传下载,需要建立ssh key

ssh-keygen   #一路回车

若创建成功,查看生成的公钥:

cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yXXXXXXXX

添加公钥至gitlab

image-20221206145031977

初始化git项目

配置git全局用户名,邮箱

git config --global user.name "YOUR NAME"
git config --global user.email "YOUR EMAIL@xxx.com"

初始化git仓库
可以通过xftp上传项目文件夹到gitlab服务器,再到目录内初始化.git仓库。

cd project_folder (项目文件夹)
git init
git remote add origin git@YOUR_IP:YOUR_NAME/project_folder.git
git add .
git commit -m "Initial commit"
git push -u origin master

这样在网页上可看到gitlab有首次commit的文件,后续也可以顺利提交和下载。