จะทำให้การอ่านและเขียนไฟล์เดียวกันในไปป์ไลน์เดียวกันเสมอ“ ล้มเหลว” ได้อย่างไร?


9

ว่าฉันมีสคริปต์ต่อไปนี้:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

ในบรรทัดสำคัญฉันอ่านและเขียนไฟล์เดียวกันtmpซึ่งบางครั้งล้มเหลว

(ฉันอ่านมันเป็นเพราะสภาพการแข่งขันเพราะกระบวนการในไปป์ไลน์ถูกดำเนินการแบบขนานซึ่งฉันไม่เข้าใจว่าทำไม - แต่ละคนheadจำเป็นต้องใช้ข้อมูลจากข้อมูลก่อนหน้านี้ใช่ไหมนี่ไม่ใช่คำถามหลักของฉัน แต่คุณสามารถตอบได้เช่นกัน)

เมื่อฉันเรียกใช้สคริปต์มันจะส่งออกประมาณ 200 บรรทัด มีวิธีใดบ้างที่ฉันสามารถบังคับให้สคริปต์นี้ให้ผลลัพธ์เสมอ 0 บรรทัด (ดังนั้นการเปลี่ยนเส้นทาง I / O ไปยังจะtmpถูกเตรียมไว้ก่อนเสมอและข้อมูลจะถูกทำลายเสมอ) เพื่อความชัดเจนฉันหมายถึงการเปลี่ยนการตั้งค่าระบบไม่ใช่สคริปต์นี้

ขอบคุณสำหรับความคิดของคุณ

คำตอบ:


2

คำตอบของ Gilles อธิบายถึงสภาพการแข่งขัน ฉันแค่จะตอบส่วนนี้:

มีวิธีใดบ้างที่ฉันสามารถบังคับให้สคริปต์นี้แสดงผลออกมาเป็น 0 บรรทัดเสมอ (ดังนั้นการเปลี่ยนเส้นทาง I / O ไปยัง tmp จะถูกเตรียมไว้ก่อนเสมอและข้อมูลจะถูกทำลายเสมอ) เพื่อความชัดเจนฉันหมายถึงการเปลี่ยนการตั้งค่าระบบ

IDK หากเครื่องมือสำหรับสิ่งนี้มีอยู่แล้ว แต่ฉันมีความคิดว่าจะใช้งานได้อย่างไร ( แต่ทราบนี้จะไม่เป็นเสมอ 0 สายเพียงการทดสอบที่มีประโยชน์ที่จับแข่งง่ายๆเช่นนี้ได้อย่างง่ายดายและบางเผ่าพันธุ์ที่มีความซับซ้อนมากขึ้น. ดูความคิดเห็น @Gilles' .) ก็จะไม่รับประกันว่าสคริปต์ปลอดภัยแต่อาจ เป็นเครื่องมือที่มีประโยชน์ในการทดสอบคล้ายกับการทดสอบโปรแกรมแบบมัลติเธรดบนซีพียูที่แตกต่างกันรวมถึงซีพียูที่ไม่ได้สั่ง x86 เช่น ARM

คุณต้องการเรียกใช้เป็น racechecker bash foo.sh

ใช้สิ่งอำนวยความสะดวกการติดตามการเรียก / การดักระบบที่เหมือนกันstrace -fและltrace -fใช้เพื่อเชื่อมต่อกับกระบวนการลูกทั้งหมด (บน Linux นี่เป็นการptraceเรียกระบบเดียวกับที่ใช้โดย GDB และ debuggers อื่น ๆเพื่อตั้งจุดพักขั้นตอนเดียวและแก้ไขหน่วยความจำ / รีจิสเตอร์ของกระบวนการอื่น)

ใช้เครื่องมือopenและการopenatเรียกระบบ: เมื่อกระบวนการใด ๆ ทำงานภายใต้เครื่องมือนี้จะทำการopen(2)เรียกระบบ (หรือopenat) ด้วยการO_RDONLYนอนหลับเป็นเวลา 1/2 หรือ 1 วินาที ปล่อยให้openระบบอื่นเรียก (โดยเฉพาะอันที่รวมถึงO_TRUNC) ดำเนินการโดยไม่ชักช้า

สิ่งนี้จะทำให้นักเขียนสามารถชนะการแข่งขันได้ในเกือบทุกสภาพการแข่งขันเว้นแต่ว่าภาระของระบบยังสูงหรือเป็นสภาพการแข่งขันที่ซับซ้อนซึ่งการตัดปลายไม่ได้เกิดขึ้นจนกว่าจะอ่านเสร็จ ดังนั้นการเปลี่ยนแปลงแบบสุ่มซึ่งopen()s (และอาจจะread()s หรือเขียน) ล่าช้าจะเพิ่มพลังการตรวจจับของเครื่องมือนี้ แต่แน่นอนโดยไม่ต้องทดสอบเป็นระยะเวลาที่ไม่มีที่สิ้นสุดด้วยตัวจำลองการหน่วงเวลาซึ่งจะครอบคลุมสถานการณ์ที่เป็นไปได้ทั้งหมดในที่สุด ในโลกแห่งความเป็นจริงคุณไม่สามารถมั่นใจได้ว่าสคริปต์ของคุณนั้นปลอดจากการแข่งขันเว้นแต่คุณจะอ่านอย่างละเอียดและพิสูจน์ว่ามันไม่ใช่


คุณอาจต้องใช้รายการที่อนุญาต (ไม่ล่าช้าopen) สำหรับไฟล์ใน/usr/binและ/usr/libกระบวนการเริ่มต้นใช้งานไม่ได้ตลอดไป (การเชื่อมโยงรันไทม์แบบไดนามิกนั้นมีopen()หลายไฟล์ (ดูstrace -eopen /bin/trueหรือ/bin/lsบางครั้ง) แม้ว่าพาเรนต์เชลล์จะทำการตัดทอนซึ่งก็ถือว่าโอเค แต่มันก็ยังดีสำหรับเครื่องมือนี้ที่จะไม่ทำให้สคริปต์ช้าลงอย่างไม่มีเหตุผล)

หรืออนุญาตให้ทุกไฟล์ที่กระบวนการเรียกไม่มีสิทธิ์ในการตัดทอนในตอนแรก เช่นกระบวนการติดตามสามารถทำการaccess(2)เรียกระบบก่อนที่จะระงับกระบวนการจริงที่ต้องการopen()ไฟล์


racecheckerตัวเองจะต้องเขียนใน C ไม่ใช่ใน shell แต่อาจใช้straceรหัสของเป็นจุดเริ่มต้นและอาจไม่ใช้งานมากในการใช้

คุณอาจจะได้รับการทำงานเช่นเดียวกับระบบแฟ้ม FUSE อาจมีตัวอย่างของ FUSE ของระบบไฟล์ passthrough แท้ๆดังนั้นคุณสามารถเพิ่มการตรวจสอบไปยังopen()ฟังก์ชันที่ทำให้มันเข้าสู่โหมดสลีปสำหรับการเปิดอ่านอย่างเดียว แต่ปล่อยให้การตัดทอนเกิดขึ้นทันที


ความคิดของคุณสำหรับตัวตรวจสอบการแข่งขันไม่ได้ผลจริงๆ ก่อนมีปัญหาว่าการหมดเวลาไม่น่าเชื่อถือ: วันหนึ่งผู้ชายคนอื่นจะใช้เวลานานกว่าที่คุณคาดไว้ (เป็นปัญหาแบบคลาสสิกกับสคริปต์สร้างหรือทดสอบซึ่งดูเหมือนจะใช้งานได้สักพักหนึ่งแล้วล้มเหลวในวิธียาก ๆ เมื่อเวิร์กโหลดขยายและหลายสิ่งรันแบบขนาน) แต่นอกเหนือจากนี้ซึ่งเปิดคุณจะเพิ่มการหน่วงเวลาให้? เพื่อที่จะตรวจจับสิ่งที่น่าสนใจคุณจะต้องทำการวิ่งจำนวนมากด้วยรูปแบบการหน่วงเวลาที่แตกต่างกันและเปรียบเทียบผลลัพธ์ของพวกเขา
Gilles 'หยุดชั่วร้าย'

@Gilles: ถูกต้องการหน่วงเวลาสั้น ๆ ที่สมเหตุสมผลไม่ได้รับประกันว่าการตัดจะชนะการแข่งขัน (บนเครื่องที่โหลดหนักในขณะที่คุณชี้ให้เห็น) แนวคิดนี้คือคุณใช้สิ่งนี้เพื่อทดสอบสคริปต์ของคุณสองสามครั้งไม่ใช่ว่าคุณจะใช้racecheckerตลอดเวลา และคุณอาจต้องการที่จะกำหนดเวลาสลีปที่เปิดเพื่ออ่านเพื่อกำหนดค่าเพื่อผลประโยชน์ของผู้คนในเครื่องที่โหลดหนักมากซึ่งต้องการตั้งค่าให้สูงขึ้นเช่น 10 วินาที หรือตั้งค่าที่ต่ำกว่าเช่น 0.1 วินาทีเป็นเวลานานหรือสคริปต์ที่ไม่มีประสิทธิภาพว่าไฟล์ใหม่เปิดจำนวนมาก
ปีเตอร์กอร์เดส

@Gilles: ความคิดที่ดีเกี่ยวกับรูปแบบการหน่วงเวลาที่แตกต่างกันซึ่งอาจทำให้คุณสามารถแข่งขันได้มากกว่าเพียงแค่สิ่งที่อยู่ภายในท่อเดียวกันที่ "ควรจะชัดเจน (เมื่อคุณรู้ว่ากระสุนทำงานอย่างไร)" เช่นกรณีของ OP แต่ "สิ่งที่เปิด?" ใด ๆ แบบอ่านอย่างเดียวที่เปิดโดยมีบัญชีปลอดภัยหรือวิธีอื่นเพื่อไม่ให้เกิดความล่าช้าในการเริ่มต้นกระบวนการ
ปีเตอร์กอร์เดส

ฉันเดาว่าคุณกำลังคิดถึงการแข่งขันที่ซับซ้อนมากขึ้นด้วยงานแบ็คกราวน์ที่ไม่ตัดทอนจนกว่ากระบวนการอื่นจะเสร็จสมบูรณ์? ใช่อาจจำเป็นต้องมีการเปลี่ยนแปลงแบบสุ่มเพื่อจับสิ่งนั้น หรืออาจดูที่แผนผังกระบวนการและหน่วงเวลา "ก่อน" อ่านเพิ่มเติมเพื่อพยายามสลับลำดับปกติ คุณสามารถทำให้เครื่องมือมีความซับซ้อนมากขึ้นในการจำลองความเป็นไปได้ในการจัดลำดับใหม่ ๆ มากขึ้น แต่ในบางจุดคุณยังต้องออกแบบโปรแกรมของคุณอย่างถูกต้องหากคุณทำงานหลายอย่างพร้อมกัน การทดสอบอัตโนมัติอาจมีประโยชน์สำหรับสคริปต์ที่ง่ายขึ้นซึ่งปัญหาที่เป็นไปได้จะมี จำกัด มากขึ้น
Peter Cordes

มันค่อนข้างคล้ายกับการทดสอบโค้ดแบบมัลติเธรดโดยเฉพาะอย่างยิ่งอัลกอริธึมแบบไม่ล็อค: เหตุผลเชิงตรรกะเกี่ยวกับเหตุผลว่าทำไมความถูกต้องจึงมีความสำคัญมากเช่นเดียวกับการทดสอบเพราะคุณไม่สามารถนับการทดสอบกับชุดเครื่องจักรใด ๆ เป็นปัญหาถ้าคุณยังไม่ได้ปิดช่องโหว่ทั้งหมด แต่เช่นเดียวกับการทดสอบบนสถาปัตยกรรมอ่อนสั่งเช่น ARM หรือ PowerPC เป็นความคิดที่ดีในการปฏิบัติทดสอบสคริปต์ภายใต้ระบบที่ล่าช้าเทียมสิ่งที่สามารถเปิดเผยบางเผ่าพันธุ์จึงดีกว่าไม่มีอะไร คุณสามารถแนะนำบั๊กที่จะไม่จับได้เสมอ!
Peter Cordes

18

ทำไมถึงมีสภาพการแข่งขัน

ทั้งสองด้านของไปป์จะถูกดำเนินการในแบบคู่ขนาน มีวิธีง่ายๆในการสาธิต: เรียกใช้

time sleep 1 | sleep 1

ใช้เวลาหนึ่งวินาทีไม่ใช่สอง

เชลล์เริ่มกระบวนการลูกสองกระบวนการและรอให้กระบวนการทั้งสองเสร็จสมบูรณ์ กระบวนการทั้งสองนี้ทำงานแบบขนาน: สาเหตุเดียวที่ทำให้กระบวนการหนึ่งทำข้อมูลให้ตรงกันกับอีกกระบวนการหนึ่งคือเมื่อต้องรอกระบวนการอื่น จุดที่พบบ่อยที่สุดของการซิงโครไนซ์คือเมื่อบล็อกด้านขวารอให้อ่านข้อมูลในอินพุตมาตรฐานและจะถูกปลดบล็อกเมื่อด้านซ้ายเขียนข้อมูลมากขึ้น การสนทนาสามารถเกิดขึ้นได้เมื่อด้านขวาช้าในการอ่านข้อมูลและบล็อกด้านซ้ายในการดำเนินการเขียนจนกระทั่งด้านขวาอ่านข้อมูลมากขึ้น (มีบัฟเฟอร์ในท่อเองจัดการโดย เคอร์เนล แต่มีขนาดสูงสุดเล็ก ๆ )

เมื่อต้องการสังเกตจุดการซิงโครไนซ์ให้สังเกตคำสั่งต่อไปนี้ ( sh -xพิมพ์แต่ละคำสั่งขณะที่เรียกใช้งาน):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

เล่นกับรูปแบบต่างๆจนกว่าคุณจะพอใจกับสิ่งที่คุณสังเกต

รับคำสั่งผสม

cat tmp | head -1 > tmp

กระบวนการทางซ้ายทำตามขั้นตอนต่อไปนี้ (ฉันเพิ่งทำรายการตามขั้นตอนที่เกี่ยวข้องกับคำอธิบายของฉัน):

  1. รันโปรแกรมภายนอกที่มีการโต้แย้งcattmp
  2. เปิดtmpให้อ่าน
  3. ในขณะที่ยังไม่ถึงจุดสิ้นสุดของไฟล์ให้อ่านอันจากไฟล์และเขียนไปยังเอาต์พุตมาตรฐาน

กระบวนการทางขวาทำสิ่งต่อไปนี้:

  1. เปลี่ยนทิศทางเอาต์พุตมาตรฐานไปที่การtmpตัดทอนไฟล์ในกระบวนการ
  2. รันโปรแกรมภายนอกที่มีการโต้แย้งhead-1
  3. อ่านหนึ่งบรรทัดจากอินพุตมาตรฐานและเขียนลงในเอาต์พุตมาตรฐาน

จุดเดียวของการซิงโครไนซ์คือ Right-3 รอให้ Left-3 ประมวลผลเต็มหนึ่งบรรทัด ไม่มีการซิงโครไนซ์ระหว่าง left-2 และ right-1 ดังนั้นพวกเขาจึงสามารถเกิดขึ้นได้ในทั้งสองคำสั่ง สิ่งที่พวกเขาเกิดขึ้นนั้นไม่สามารถคาดเดาได้: มันขึ้นอยู่กับสถาปัตยกรรมของ CPU, บนเชลล์, ในเคอร์เนล, ซึ่งแกนประมวลผลเกิดขึ้นตามกำหนดเวลา, ในสิ่งที่อินเตอร์รัปต์ที่ CPU ได้รับในช่วงเวลานั้นเป็นต้น

วิธีการเปลี่ยนพฤติกรรม

คุณไม่สามารถเปลี่ยนพฤติกรรมได้โดยเปลี่ยนการตั้งค่าระบบ คอมพิวเตอร์ทำในสิ่งที่คุณบอกให้ทำ คุณบอกให้ตัดทอนtmpและอ่านจากtmpแบบขนานดังนั้นทั้งสองอย่างขนานกัน

ตกลงมี "การตั้งค่าระบบ" หนึ่งที่คุณสามารถเปลี่ยนได้: คุณสามารถแทนที่/bin/bashด้วยโปรแกรมอื่นที่ไม่ใช่การทุบตี ฉันหวังว่ามันจะไปโดยไม่บอกว่านี่ไม่ใช่ความคิดที่ดี

หากคุณต้องการให้การตัดปลายเกิดขึ้นก่อนทางด้านซ้ายของไพพ์คุณต้องวางไว้ด้านนอกไพพ์ไลน์เช่น:

{ cat tmp | head -1; } >tmp

หรือ

( exec >tmp; cat tmp | head -1 )

ฉันไม่รู้ว่าทำไมคุณถึงต้องการสิ่งนี้ จุดใดในการอ่านไฟล์ที่คุณรู้ว่าว่างเปล่า

ในทางกลับกันหากคุณต้องการให้การเปลี่ยนเส้นทางเอาต์พุต (รวมถึงการตัดปลาย) เกิดขึ้นหลังจากcatอ่านเสร็จแล้วคุณต้องบัฟเฟอร์ข้อมูลในหน่วยความจำเช่น

line=$(cat tmp | head -1)
printf %s "$line" >tmp

หรือเขียนไปยังไฟล์อื่นแล้วย้ายเข้าที่ นี่เป็นวิธีที่ดีในการทำสิ่งต่าง ๆ ในสคริปต์และมีข้อได้เปรียบที่ไฟล์เขียนเต็มก่อนที่จะสามารถมองเห็นได้ด้วยชื่อดั้งเดิม

cat tmp | head -1 >new && mv new tmp

moreutilsspongeคอลเลกชันรวมถึงโปรแกรมที่ไม่เพียงแค่นั้นเรียกว่า

cat tmp | head -1 | sponge tmp

วิธีการตรวจสอบปัญหาโดยอัตโนมัติ

หากเป้าหมายของคุณคือการเขียนสคริปต์ที่ไม่ดีและคิดออกโดยอัตโนมัติว่าพวกเขาแตกหักอย่างไรเสียใจด้วยชีวิตไม่ใช่เรื่องง่าย การวิเคราะห์รันไทม์จะไม่พบปัญหาได้อย่างน่าเชื่อถือเพราะบางครั้งcatเสร็จสิ้นการอ่านก่อนที่จะตัดทอน การวิเคราะห์เชิงสถิตในหลักการทำได้ ตัวอย่างที่ง่ายในคำถามของคุณถูกตรวจจับโดยShellcheckแต่อาจไม่พบปัญหาที่คล้ายกันในสคริปต์ที่ซับซ้อนกว่านี้


นั่นคือเป้าหมายของฉันเพื่อตรวจสอบว่าสคริปต์เขียนดีหรือไม่ หากสคริปต์อาจทำลายข้อมูลด้วยวิธีนี้ฉันแค่อยากให้ทำลายข้อมูลทุกครั้ง ไม่ดีที่จะได้ยินว่านี่เป็นไปไม่ได้เกือบ ขอบคุณคุณตอนนี้ฉันรู้ว่าปัญหาคืออะไรและจะพยายามคิดวิธีแก้ปัญหา
karlosss

@ Karlosss: อืมฉันสงสัยว่าคุณสามารถใช้ระบบการติดตามการโทร / การสกัดกั้นสิ่งต่าง ๆ เช่นstrace(เช่น Linux ptrace) เพื่อให้openการโทรทั้งหมดของระบบสำหรับการอ่าน (ในกระบวนการลูกทั้งหมด) หลับไปครึ่งวินาทีดังนั้นเมื่อแข่งกับ การตัดทอนการตัดทอนมักจะชนะ
Peter Cordes

@PeterCordes ฉันเป็นมือใหม่ในการนี้ถ้าคุณสามารถจัดการวิธีการเพื่อให้บรรลุและเขียนมันเป็นคำตอบฉันจะยอมรับมัน
karlosss

@PeterCordes คุณไม่สามารถรับประกันได้ว่าการตัดทอนจะชนะด้วยความล่าช้า มันจะทำงานได้เกือบตลอดเวลา แต่บางครั้งบนเครื่องที่มีการโหลดจำนวนมากสคริปต์ของคุณจะล้มเหลวด้วยวิธีลึกลับมากกว่าหรือน้อยกว่า
Gilles 'หยุดชั่วร้าย'

@Gilles: มาคุยเรื่องนี้กันดีกว่า
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.